How do I change only the status code on a Spring MVC error with Boot?

968 Views Asked by At

I'm writing a Web application that makes downstream calls using RestTemplate. If the underlying service returns a 401 Unauthorized, I want to also return a 401 to the calling application; the default behavior is to return a 500. I want to keep the default Spring Boot error response as provided by BasicErrorController; the only change I want is to set the status code.

In custom exceptions, I'd just annotate the exception class with @ResponseStatus, but I can't do that here because HttpClientErrorException.Unauthorized is provided by Spring. I tried two approaches with @ControllerAdvice:

@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
@ResponseStatus(UNAUTHORIZED)
public void returnsEmptyBody(HttpClientErrorException.Unauthorized ex) {
}

@ExceptionHandler(HttpClientErrorException.Unauthorized.class)
@ResponseStatus(UNAUTHORIZED)
public void doesNotUseBasicErrorController(HttpClientErrorException.Unauthorized ex) {
    throw new RuntimeException(ex);
}

How can I configure MVC to continue to use all of the built-in Boot error handling except for explicitly overriding the status code?

2

There are 2 best solutions below

1
On

You can customize the error handler of the RestTemplate to throw your custom exception, and then handle that exception with the @ControllerAdvice as you mentioned.

Something like this:

@Configuration
public class RestConfig {
    
    @Bean
    public RestTemplate restTemplate(){
        
        // Build rest template
        RestTemplate res = new RestTemplate();
        res.setErrorHandler(new MyResponseErrorHandler());
        return res;
    }
    
    private class MyResponseErrorHandler extends DefaultResponseErrorHandler {

        @Override
        public void handleError(ClientHttpResponse response) throws IOException {
            if (HttpStatus.UNAUTHORIZED.equals(response.getStatusCode())) {
                // Throw your custom exception here
            }
        }
    }
}
1
On

The below code works for me -- in an app consisting of a @RestController whose one method consisted of throw new HttpClientException(HttpStatus.UNAUTHORIZED), running on an embedded Tomcat. If you're running on a non-embedded Tomcat (or, I suspect, on an embedded non-Tomcat) odds are you'll have to do something at least somewhat different, but I hope this answer is at least somewhat helpful anyway.

@ControllerAdvice
public class Advisor {
  @ExceptionHandler(HttpClientException.class)
  public String handleUnauthorizedFromApi(HttpClientException ex, HttpServletRequest req) {
    if (/* ex instanceof HttpClientException.Unauthorized or whatever */) {
      req.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 401);
    }
    return "forward:/error";
  }
}

Explanation: when a HttpClientException is thrown while we're processing request X (in an embedded servlet), what normally happens is that it bubbles all the way up to some org.apache class. (I might fire the debugger up again and work out which one, but this is a pretty high-level explanation so it doesn't matter much.) That class then sends request X back to the application, except this time the request goes to "/error", not to wherever it was originally going. In a Spring Boot app (as long as you don't turn some autoconfiguration off), that means that request X is ultimately processed by some method in BasicErrorController.

OK, so why does this whole system send a 500 to the client unless we do something? Because that org.apache class mentioned above sets something on request X which says "processing this went wrong". It is right to do so: processing request X did, after all, result in an exception which the servlet container had to catch. As far as the container is concerned, the app messed up.

So we want to do a couple of things. First, we want the servlet container to not think we messed up. We achieve this by telling Spring to catch the exception before it reaches the container, ie by writing an @ExceptionHandler method. Second, we want the request to go to "/error" even though we caught the exception. We achieve this by the simple method of sending it there ourselves, via a forward. Third, we want the BasicErrorController to set the correct status and message on the response it sends. It turns out that BasicErrorController (working in tandem with its immediate superclass) looks at an attribute on the request to determine what status code to send to the client. (Figuring this out requires reading the class's source code, but that source code is on github and perfectly readable.) We therefore set that attribute.

EDIT: I got a bit carried away writing this and forgot to mention that I don't think using this code is good practice. It ties you to some implementation details of BasicErrorController, and it's just not the way that the Boot classes are expected to be used. Spring Boot generally assumes that you want it to handle your error completely or not at all; this is a reasonable assumption, too, since piecemeal error handling is generally not a great idea. My recommendation to you -- even if the code above (or something like it) does wind up working -- is to write an @ExceptionHandler that handles the error completely, meaning it sets both status and response body and doesn't forward to anything.