Search code examples
javaspring-bootnetflix-zuulspring-cloud-netflix

Netflix's Zuul Error filter implementation


What would be an example implementation of an error filter for netflix's zuul gateway service? All the examples I have found have been either incorrect or too old to work.


Solution

  • You can create your own custom filter and put that filter to be executed right before Zuul's default SendErrorFilter, but sometimes the default filter will override your body or HTTP error. I prefer to disable the default filter by putting this in application properties:

    zuul.SendErrorFilter.error.disable=true
    

    Then create your own CustomSendErrorFilter by extending the default one. An example implementation would be:

    @Component
    public class SendErrorCustomFilter extends SendErrorFilter {
    
        private static final Logger LOG = LoggerFactory.getLogger(SendErrorCustomFilter.class);
        private static final String SERVLET_ERROR_STATUS_CODE = "javax.servlet.error.status_code";
        private static final String SERVLET_ERROR_EXCEPTION = "javax.servlet.error.exception";
        private static final String SERVLET_ERROR_MESSAGE = "javax.servlet.error.message";
    
        @Value("${error.path:/error}")
        private String errorPath;
    
        @Override
        public Object run() {
            try {
                RequestContext ctx = RequestContext.getCurrentContext();
                ExceptionHolder exception = findZuulException(ctx.getThrowable());
                HttpServletRequest request = ctx.getRequest();
                Throwable cause = exception.getThrowable().getCause();
                int statusCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
    
                if (causeIsIOError(cause)) {
                    statusCode = HttpServletResponse.SC_CONFLICT;
                } else if (causeIsAuthorizationError(cause)) {
                    statusCode = HttpServletResponse.SC_UNAUTHORIZED;
                }
    
                request.setAttribute(SERVLET_ERROR_STATUS_CODE, statusCode);
    
                LOG.warn("Error during filtering", cause);
                request.setAttribute(SERVLET_ERROR_EXCEPTION, cause);
    
                if (StringUtils.hasText(exception.getErrorCause())) {
                    request.setAttribute(SERVLET_ERROR_MESSAGE, cause.getMessage());
                }
    
                RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath);
                if (dispatcher != null) {
                    ctx.set(SEND_ERROR_FILTER_RAN, true);
                    if (!ctx.getResponse().isCommitted()) {
                        ctx.setResponseStatusCode(exception.getStatusCode());
                        dispatcher.forward(request, ctx.getResponse());
                    }
                }
    
            } catch (Exception ex) {
                ReflectionUtils.rethrowRuntimeException(ex);
            }
            return null;
        }
    
        private boolean causeIsIOError(Throwable cause) {
            return cause instanceof InvalidTokenPayloadException
                    || cause instanceof InvalidResponseBodyException;
        }
    
        public boolean causeIsAuthorizationError(Throwable cause) {
            return cause instanceof InvalidJWTTokenException ||
                    cause instanceof NoPermissionForResourceException ||
                    cause instanceof MissingAuthorizationHeaderException;
        }
    

    This way you have all the control for the error that is going to be sent back to the client. I have extracted a few methods that check for different kinds of Exceptions and put different HTTP errors depending on these Exceptions. I am using the cause of the exception because these exceptions are wrapped inside a ZuulException.