How to make @ResponseStatus exceptions work with custom exception handlers in Spring

Problem

Since version 3.0, Spring lets us mark an exception class with the status code and reason that should be returned. For example, throwing the following exception will return HTTP response status code 403:

@ResponseStatus(HttpStatus.FORBIDDEN)
class ForbiddenException extends RuntimeException { 
	super("Boom!");
}

Depending on what HTTP server you use and how it’s configured, you will see something like this:

Now imagine you have a custom catch-all exception handler that returns exception message as JSON:

@ControllerAdvice
class ExceptionHandlerAdvice {
	@ExceptionHandler
    @ResponseBody
	ExceptionRepresentation handle(Exception exception) {
		return new ExceptionRepresentation(exception.getLocalizedMessage());
    }
}

class ExceptionRepresentation {
	String message;
   	// constructor goes here
}

And a controller that throws ForbiddenException:

@RestController
class TheForbidder {
	@RequestMapping("/forbid")
	String forbid() {
		throw new ForbiddenException();
	}
}

What do you expect to be returned from the controller upon invocation?

I’d expect response code 403 and the following body:

{

	"message": "Boom!"

}

Surprisingly, the returned body is correct, but the response code is 200. Why?

Let’s see what @ResponseStatus JavaDocs says:

Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the HttpServletResponse.sendError method will be used.

With HttpServletResponse.sendError, the response is considered complete and should not be written to any further. Furthermore, the Servlet container will typically write an HTML error page therefore making the use of a reason unsuitable for REST APIs. For such cases it is preferable to use a org.springframework.http.ResponseEntity as a return type and avoid the use of @ResponseStatus altogether.

JavaDocs suggests avoiding the use of @ResponseStatus for Web APIs, but we don’t want to surrender without a fight. Here is why:

The statement is true only for the default ResponseStatusExceptionResolver exception handler. By introducing @ControllerAdvice, we have overridden the default exception handler with ExceptionHandlerExceptionResolver and now can easily make our custom exception handler work with annotated exceptions.

Solution

Here is how our custom exception handler may look look:

import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation

@ControllerAdvice
class ExceptionHandlerAdvice {
	@ExceptionHandler
    @ResponseBody
	ExceptionRepresentation handle(Exception exception) {
		ExceptionRepresentation body = new ExceptionRepresentation(exception.getLocalizedMessage());
        HttpStatus responseStatus = resolveAnnotatedResponseStatus(exception);
        return new ResponseEntity<ExceptionRepresentation>(body, responseStatus);
    }
    
    HttpStatus resolveAnnotatedResponseStatus(Exception exception) {
        ResponseStatus annotation = findMergedAnnotation(exception.getClass(), ResponseStatus.class);
        if (annotation != null) {
            return annotation.value();
        }
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

findMergedAnnotation respects the hierarchy of exceptions and let us inherit @ResponseCode annotation from the base class and override it at the subclass level if necessary.

Beware that as of Spring 4.2.2, the default exception resolver looks recursively for @ResponseStatus present on cause exception (see ResponseStatusExceptionResolver). This functionality can be easily re-implemented in our custom controller if needed.

Final words

Hope this was helpful and now you don’t have to get rid of valuable @ResponseStatus in your Web APIs.

Please share this article if you liked it and don't forget to follow me on Twitter. Ah, don't be shy to comment ↓ as well!