Springboot error handling HttpServerErrorException, /error, etc

Viewed 285

I'm having a bit of trouble getting a complete error handling solution. I've created a @ControllerAdvice class that catches Exceptions and QueryTimeException. However, the 404 Not Found message does not go to my custom page. And the HttpStatus.GATEWAY_TIMEOUT doesn't get routed properly. However, HttpStatus.REQUEST_TIMEOUT and Exceptions do get routed correctly.

Here's my Error Controller. What am I missing or doing incorrectly?

@ControllerAdvice
public class MyErrorController implements ErrorController {

    public static final Logger logger = LoggerFactory.getLogger(MyErrorController.class);

    @ExceptionHandler(HttpServerErrorException.GatewayTimeout.class)
    @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
    public String gatewayTimeout() {
        return "errors/error-timeout";
    }

    @ExceptionHandler(QueryTimeoutException.class)
    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
    public String requestTimeout() {
        return "errors/error-timeout";
    }

    @ExceptionHandler(value = {HttpClientErrorException.class, Exception.class})
    public String handleExceptions() {
        return  "errors/error";
    }

    @RequestMapping("/error")
    public String handleErrors(HttpServletRequest request, Exception ex) {
        return  "errors/error";
    }

    // This method is deprecated, so do not add the @Override annotation
    // However, the method must remain while the Spring team has not yet removed it from the ErrorController interface
    public String getErrorPath() {
        return "/error";
    }

}

I've added the following to application.properties:

server.error.whitelabel.enabled=true
server.error.path=/error

To test the above, I added the following methods to a Controller.

    @GetMapping("/throw504")
    public ResponseEntity<Void> throwGatewayTimeout() {
        throw new HttpServerErrorException(HttpStatus.GATEWAY_TIMEOUT);
    }

    @GetMapping("/throw408")
    public ResponseEntity<Void> throwResponseTimeout() {
        throw new QueryTimeoutException();
    }

    @GetMapping("/throw500")
    public ResponseEntity<Void> throwNotImplemented() {
        throw new HttpServerErrorException(HttpStatus.NOT_IMPLEMENTED);
    }

So I test by running my app, the navigating to localhost:8080/throw408 (I get the error-timeout page), localhost:8080/throw500 (I get the error page), which are both correct.

I then test localhost:8080/throw504 which incorrectly opens the error page, not error-timeout as I expected. I then test localhost:8080/jskld, which displays a generic 404 page, not the error page I expected.

1 Answers

Let's analyze this a bit more, and you will probably see the error in your code.

 @ExceptionHandler(HttpServerErrorException.GatewayTimeout.class)
    @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
    public String gatewayTimeout() {
        return "errors/error-timeout";
    }

The exception handler waits for an instance of GatewayTimeout.class to handle this exception.

When you create this exception in your controller though

@GetMapping("/throw504")
    public ResponseEntity<Void> throwGatewayTimeout() {
        throw new HttpServerErrorException(HttpStatus.GATEWAY_TIMEOUT); <------
    }

If you inspect this class you will find

public class HttpServerErrorException extends HttpStatusCodeException {
    private static final long serialVersionUID = -2915754006618138282L;

    public HttpServerErrorException(HttpStatus statusCode) {
        super(statusCode);
    }

So the constructor that you use returns back a HttpServerErrorException.class

Not a GatewayTimeout.class

GatewayTimeout.class is a static nested class which extends HttpServerErrorException.class.

public static final class GatewayTimeout extends HttpServerErrorException {
        private GatewayTimeout(String statusText, HttpHeaders headers, byte[] body, @Nullable Charset charset) {
            super(HttpStatus.GATEWAY_TIMEOUT, statusText, headers, body, charset);
        }

If you look closer in HttpServerErrorException.class there is a constructor that creates an instance of GatewayTimeout.class

One way would be to use the constructor

HttpServerErrorException.create(HttpStatus.GATEWAY_TIMEOUT,"status text", new HttpHeaders(), new byte[] {}, Charset.defaultCharset());

This will return you an exception instance of GatewayTimeout.class which spring would probably be able to match.

So your controller must be adapted to be something like

  @GetMapping("/throw504")
  public ResponseEntity<Void> throwGatewayTimeout() {
    throw HttpServerErrorException.create(HttpStatus.GATEWAY_TIMEOUT,"status text", new HttpHeaders(), new byte[] {}, Charset.defaultCharset());
   }

In the end the point is that GatewayTimeout.class extends from HttpServerErrorException. So this means that GatewayTimeout.class is an HttpServerErrorException, but an HttpServerErrorException would not be for sure a GatewayTimeout.class.

Related