Spring Cloud: Zuul error handling

Have you ever bothered to understand how Spring Cloud Zuul proxy behaves in case of error? If everything works fine, your successful or errored request gets proxied, but what in cases that Zuul doesn’t have definition for specific service id, the connection itself can’t be established or the hystrix circut is opened? There are some edge cases in Zuul implementation that it is worth to know about.

Let’s see what happens if your Zuul proxy can not access the service from the discovery client. Simply because no service has been registered, or because all of the nodes are in DOWN state – for some reason. You might be suprised by the fact that when an error will be trigger in one of Zuul filters, it won’t be handled by your Spring registered exception handlers. Instead the exception will be propagated through servlet context up to your ‘/error’ mapped handler (though I had observed incosistent default behavior across different servlet containers). So if you run your app on Tomcat you might see an error page completly similar to the one below.

zuul_tomcat

Everything like you would expect? An HTTP 500 error status and default page that exposes the application stack trace to the client.

Let’s redo this “test” on Undertow (1.2.12), this time we have empty page and HTTP 200 – Success status. (This was actual cause of notorius error. Let’s assume that the the user just POST-ed “Login” information to Zuul proxied backend and recieved 200 due to error in forwarding. Your would rather not want that to happen in production.)

zuul_undertow

Both of the behaviours are far from perfect. What if only what you wanted to do is perform AJAX call through Zuul and this behaviour brakes you API? What if you coded your client with error handling logic that in cases of error would expect a JSON payload with the error details?

So let’s see how we can fix this. The Zuul error handling is being handled through RibbonRoutingFilter and SendErrorFilter and will forward any errored request to ${error.path}, which defaults to ‘/error’. In case you relay on the defaults that will be handled by Spring Boot’s BasicErrorController. You can overide this behaviour and implement your own ErrorController. Let’s try with one assumption, since we using Zuul reverse proxy preaty much all of our application is only serving static content (scripts, css, html etc.) so we only will be interested in returning an error to our frontent in easy to understand form. For this purpose let’s create a Vnd.error representation of the error.

Here is how we do this:

@Controller
public class VndErrorController implements ErrorController {

    @Value("${error.path:/error}")
    private String errorPath;
    
    @Override
    public String getErrorPath() {
        return errorPath;
    }

    @RequestMapping(value = "${error.path:/error}", produces = "application/vnd.error+json")
    public @ResponseBody ResponseEntity error(HttpServletRequest request) {

        final String logref = RequestCorrelationUtils.getCurrentCorrelationId();
        final int status = getErrorStatus(request);
        final String errorMessage = getErrorMessage(request);
        final VndError error = new VndError(logref, errorMessage);
        return ResponseEntity.status(status).body(error);
    }

    private int getErrorStatus(HttpServletRequest request) {
        Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
        return statusCode != null ? statusCode : HttpStatus.INTERNAL_SERVER_ERROR.value();
    }

    private String getErrorMessage(HttpServletRequest request) {
        final Throwable exc = (Throwable) request.getAttribute("javax.servlet.error.exception");
        return exc != null ? exc.getMessage() : "Unexpected error occurred";
    }
}

You may notice the RequestCorrelationUtils.getCurrentCorrelationId method from the previous post. What we done here is implementation of custom error handling controller that creates a VndError based on the servlet error attributes and returns it as response payload.

Undertow setup

Undertow is bit different and will require special handling. By default it dissalows handling request wrappers and throws exception in case that there are used. You need to configure it correctly in order to use it with Zuul.

    @Bean
    public UndertowEmbeddedServletContainerFactory embeddedServletContainerFactory() {
        UndertowEmbeddedServletContainerFactory factory = new UndertowEmbeddedServletContainerFactory();
        factory.addDeploymentInfoCustomizers(new UndertowDeploymentInfoCustomizer() {
            @Override
            public void customize(DeploymentInfo deploymentInfo) {
                deploymentInfo.setAllowNonStandardWrappers(true);
            }
        });
        return factory;
    }

We need to customize our deployment and set allow-non-standard-wrappers to true.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s