25

I have a scenario in Zuul where the service that the URL is routed too might be down . So the reponse body gets thrown with 500 HTTP Status and ZuulException in the JSON body response.

{
  "timestamp": 1459973637928,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "com.netflix.zuul.exception.ZuulException",
  "message": "Forwarding error"
}

All I want to do is to customise or remove the JSON response and maybe change the HTTP status Code.

I tried to create a exception Handler with @ControllerAdvice but the exception is not grabbed by the handler.

UPDATES:

So I extended the Zuul Filter I can see it getting into the run method after the error has been executed how do i change the response then. Below is what i got so far. I read somewhere about SendErrorFilter but how do i implement that and what does it do?

public class CustomFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {

        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        final RequestContext ctx = RequestContext.getCurrentContext();
        final HttpServletResponse response = ctx.getResponse();
        if (HttpStatus.INTERNAL_SERVER_ERROR.value() == ctx.getResponse().getStatus()) {
            try {
                response.sendError(404, "Error Error"); //trying to change the response will need to throw a JSON body.
            } catch (final IOException e) {
                e.printStackTrace();
            } ;
        }

        return null;
    }

Added this to the class that has @EnableZuulProxy

@Bean
public CustomFilter customFilter() {
    return new CustomFilter();
}
Grinish Nepal
  • 3,037
  • 3
  • 30
  • 49
  • Have you already tried anything? – Aritz Apr 06 '16 at 20:27
  • I tried to add a Exception Handler by annotating the class with @ControllerAdvice but that doesnot seem to work. I think i need to do something with Zuul Filters but not sure what needs to happen. – Grinish Nepal Apr 06 '16 at 20:33
  • 1
    OK, then it would be good to have your question edited in order to put this attempts, cause as you can notice there's some downvoter who thought you haven't tried anything yourself. If you improve the question I'll give you my +1 as I consider it an interesting subject. – Aritz Apr 06 '16 at 20:36
  • done updated the question. – Grinish Nepal Apr 06 '16 at 20:42
  • 2
    Coding a custom `ErrorController` implementation may also help someone to tackle with Forwarding error: https://jmnarloch.wordpress.com/2015/09/16/spring-cloud-zuul-error-handling/ – Vladimir Salin Jun 08 '17 at 13:14

7 Answers7

27

We finally got this working [Coded by one of my colleague]:-

public class CustomErrorFilter extends ZuulFilter {

    private static final Logger LOG = LoggerFactory.getLogger(CustomErrorFilter.class);
    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return -1; // Needs to run before SendErrorFilter which has filterOrder == 0
    }

    @Override
    public boolean shouldFilter() {
        // only forward to errorPath if it hasn't been forwarded to already
        return RequestContext.getCurrentContext().containsKey("error.status_code");
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            Object e = ctx.get("error.exception");

            if (e != null && e instanceof ZuulException) {
                ZuulException zuulException = (ZuulException)e;
                LOG.error("Zuul failure detected: " + zuulException.getMessage(), zuulException);

                // Remove error code to prevent further error handling in follow up filters
                ctx.remove("error.status_code");

                // Populate context with new response values
                ctx.setResponseBody(“Overriding Zuul Exception Body”);
                ctx.getResponse().setContentType("application/json");
                ctx.setResponseStatusCode(500); //Can set any error code as excepted
            }
        }
        catch (Exception ex) {
            LOG.error("Exception filtering in custom error filter", ex);
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }
}
Grinish Nepal
  • 3,037
  • 3
  • 30
  • 49
  • can you advise how can redirect to standard error page for any exception occurred at zuul layer as I dont want to hardcode ReponseBody. Thanks @grinish-nepal – Joey Trang Jun 15 '17 at 07:44
  • so you did not add **error** filter, and only post filter. – Bruce Lee Sep 03 '17 at 12:23
  • It is a post filter but it runs before the sendErrorFilter you can see that in the comment. – Grinish Nepal Oct 17 '17 at 18:11
  • 1
    latest version of zuul has not `error.exception` or `error.status_code`. instead use `throwable`. by the way if you want replace the error response then use `erro` filter type instead of `post` – sancho Aug 23 '20 at 13:22
11

The Zuul RequestContext doesn't contain the error.exception as mentioned in this answer.
Up to date the Zuul error filter:

@Component
public class ErrorFilter extends ZuulFilter {
    private static final Logger LOG = LoggerFactory.getLogger(ErrorFilter.class);

    private static final String FILTER_TYPE = "error";
    private static final String THROWABLE_KEY = "throwable";
    private static final int FILTER_ORDER = -1;

    @Override
    public String filterType() {
        return FILTER_TYPE;
    }

    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        final RequestContext context = RequestContext.getCurrentContext();
        final Object throwable = context.get(THROWABLE_KEY);

        if (throwable instanceof ZuulException) {
            final ZuulException zuulException = (ZuulException) throwable;
            LOG.error("Zuul failure detected: " + zuulException.getMessage());

            // remove error code to prevent further error handling in follow up filters
            context.remove(THROWABLE_KEY);

            // populate context with new response values
            context.setResponseBody("Overriding Zuul Exception Body");
            context.getResponse().setContentType("application/json");
            // can set any error code as excepted
            context.setResponseStatusCode(503);
        }
        return null;
    }
}
xxxception
  • 915
  • 10
  • 6
10

I had the same problem and was able to solve it in simpler way

Just put this into you Filter run() method

    if (<your condition>) {
        ZuulException zuulException = new ZuulException("User message", statusCode, "Error Details message");
        throw new ZuulRuntimeException(zuulException);
    }

and SendErrorFilter will deliver to the user the message with the desired statusCode.

This Exception in an Exception pattern does not look exactly nice, but it works here.

Vasile Rotaru
  • 452
  • 4
  • 11
4

Forwarding is often done by a filter, in this case the request does not even reach a controller. This would explain why your @ControllerAdvice does not work.

If you forward in the controller than the @ControllerAdvice should work. Check if spring creates an instance of the class annotated with @ControllerAdvice. For that place a breakpoint in the class and see whether it is hit.

Add a breakpoint also in the controller method where the forwarding should happen. May be you accidently invoke another controller method than you inspect ?

These steps should help you resolve the issue.

In your class annotated with @ControllerAdvice add an ExceptionHandler method annotated with @ExceptionHandler(Exception.class), that should catch every Exception.

EDIT : You can try to add your own filter that converts the error response returned by the Zuulfilter. There you can change the response as you like.

How the error response can be customized is explained here :

exception handling for filter in spring

Placing the filter correctly may be a little tricky. Not exactly sure about the correct position, but you should be aware of the order of your filters and the place where you handle the exception.

If you place it before the Zuulfilter, you have to code your error handling after calling doFilter().

If you place it after the Zuulfilter, you have to code your error handling before calling doFilter().

Add breakpoints in your filter before and after doFilter() may help to find the correct position.

rjdkolb
  • 10,377
  • 11
  • 69
  • 89
  • Actually I am not forwarding anything here. In my sporing boot application all I have is @EnableZullProxy from spring-cloud so that I can add my routing to the configuration. So there is no controller in there. I was hoping that the controllerAdvice might work but since the forwarding is done by the filter I need to grab that part and customize it which i could not figure out how. – Grinish Nepal Apr 06 '16 at 21:11
3

Here are the steps to do it with @ControllerAdvice:

  1. First add a filter of type error and let it be run before the SendErrorFilter in zuul itself.
  2. Make sure to remove the key associated with the exception from the RequestContext to prevent the SendErrorFilter from executing.
  3. Use RequestDispatcher to forward the request to the ErrorController -- explained below.
  4. Add a @RestController class and make it extends AbstractErrorController, and re-throw the exception again (add it in the step of executing your new error filter with (key, exception), get it from the RequestContext in your controller).

The exception will now be caught in your @ControllerAdvice class.

  • this actually works, except that i implemented `ErrorController` to the `ControllerAdvice` class and add the `RestController` annotation. This maybe not cool, but it works. – Bruce Lee Sep 04 '17 at 08:25
2
    The simplest solution is to follow first 4 steps.


     1. Create your own CustomErrorController extends
        AbstractErrorController which will not allow the
        BasicErrorController to be called.
     2. Customize according to your need refer below method from
        BasicErrorController.

    <pre><code> 
        @RequestMapping
        public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
            Map<String, Object> body = getErrorAttributes(request,
                    isIncludeStackTrace(request, MediaType.ALL));
            HttpStatus status = getStatus(request);
            return new ResponseEntity<>(body, status);
        }
    </pre></code> 

     4. You can control whether you want exception / stack trace to be printed or not can do as mentioned below:
    <pre><code>
    server.error.includeException=false
    server.error.includeStacktrace=ON_TRACE_PARAM
    </pre></code>
 ====================================================

    5. If you want all together different error response re-throw your custom exception from your CustomErrorController and implement the Advice class as mentioned below:

    <pre><code>

@Controller
@Slf4j
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
            List<ErrorViewResolver> errorViewResolvers) {

        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
        log.info("Created");
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        throw new CustomErrorException(String.valueOf(status.value()), status.getReasonPhrase(), body);
    }
}


    @ControllerAdvice
    public class GenericExceptionHandler {
    // Exception handler annotation invokes a method when a specific exception
        // occurs. Here we have invoked Exception.class since we
        // don't have a specific exception scenario.
        @ExceptionHandler(CustomException.class)
        @ResponseBody
        public ErrorListWsDTO customExceptionHandle(
                final HttpServletRequest request,
                final HttpServletResponse response,
                final CustomException exception) {
                LOG.info("Exception Handler invoked");
                ErrorListWsDTO errorData = null;
                errorData = prepareResponse(response, exception);
                response.setStatus(Integer.parseInt(exception.getCode()));
                return errorData;
        }

        /**
         * Prepare error response for BAD Request
         *
         * @param response
         * @param exception
         * @return
         */
        private ErrorListWsDTO prepareResponse(final HttpServletResponse response,
                final AbstractException exception) {
                final ErrorListWsDTO errorListData = new ErrorListWsDTO();
                final List<ErrorWsDTO> errorList = new ArrayList<>();
                response.setStatus(HttpStatus.BAD_REQUEST.value());
                final ErrorWsDTO errorData = prepareErrorData("500",
                        "FAILURE", exception.getCause().getMessage());
                errorList.add(errorData);
                errorListData.setErrors(errorList);
                return errorListData;
        }

        /**
         * This method is used to prepare error data
         *
         * @param code
         *            error code
         * @param status
         *            status can be success or failure
         * @param exceptionMsg
         *            message description
         * @return ErrorDTO
         */
        private ErrorWsDTO prepareErrorData(final String code, final String status,
                final String exceptionMsg) {

                final ErrorWsDTO errorDTO = new ErrorWsDTO();
                errorDTO.setReason(code);
                errorDTO.setType(status);
                errorDTO.setMessage(exceptionMsg);
                return errorDTO;
        }

    }
    </pre></code>
Ravi Gupta
  • 81
  • 5
1

This is what worked for me. RestExceptionResponse is the class which is used within the @ControllerAdvice, so we have an identical exception response in case of internal ZuulExceptions.

@Component
@Log4j
public class CustomZuulErrorFilter extends ZuulFilter {

    private static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";

    @Override
    public String filterType() {
        return ERROR_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_ERROR_FILTER_ORDER - 1; // Needs to run before SendErrorFilter which has filterOrder == 0
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable ex = ctx.getThrowable();
        return ex instanceof ZuulException && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            ZuulException ex = (ZuulException) ctx.getThrowable();

            // log this as error
            log.error(StackTracer.toString(ex));

            String requestUri = ctx.containsKey(REQUEST_URI_KEY) ? ctx.get(REQUEST_URI_KEY).toString() : "/";
            RestExceptionResponse exceptionResponse = new RestExceptionResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex, requestUri);

            // Populate context with new response values
            ctx.setResponseStatusCode(500);
            this.writeResponseBody(ctx.getResponse(), exceptionResponse);

            ctx.set(SEND_ERROR_FILTER_RAN, true);
        }
        catch (Exception ex) {
            log.error(StackTracer.toString(ex));
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }


    private void writeResponseBody(HttpServletResponse response, Object body) throws IOException {
        response.setContentType("application/json");
        try (PrintWriter writer = response.getWriter()) {
            writer.println(new JSonSerializer().toJson(body));
        }
    }
}

The output looks like this:

{
    "timestamp": "2020-08-10 16:18:16.820",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/service",
    "exception": {
        "message": "Filter threw Exception",
        "exceptionClass": "com.netflix.zuul.exception.ZuulException",
        "superClasses": [
            "com.netflix.zuul.exception.ZuulException",
            "java.lang.Exception",
            "java.lang.Throwable",
            "java.lang.Object"
        ],
        "stackTrace": null,
        "cause": {
            "message": "com.netflix.zuul.exception.ZuulException: Forwarding error",
            "exceptionClass": "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
            "superClasses": [
                "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
                "java.lang.RuntimeException",
                "java.lang.Exception",
                "java.lang.Throwable",
                "java.lang.Object"
            ],
            "stackTrace": null,
            "cause": {
                "message": "Forwarding error",
                "exceptionClass": "com.netflix.zuul.exception.ZuulException",
                "superClasses": [
                    "com.netflix.zuul.exception.ZuulException",
                    "java.lang.Exception",
                    "java.lang.Throwable",
                    "java.lang.Object"
                ],
                "stackTrace": null,
                "cause": {
                    "message": "Load balancer does not have available server for client: template-scalable-service",
                    "exceptionClass": "com.netflix.client.ClientException",
                    "superClasses": [
                        "com.netflix.client.ClientException",
                        "java.lang.Exception",
                        "java.lang.Throwable",
                        "java.lang.Object"
                    ],
                    "stackTrace": null,
                    "cause": null
                }
            }
        }
    }
}
Peter Nagy
  • 86
  • 1
  • 7