Handling Exceptions — Extending BasicErrorController And DefaultErrorAttributes
In the last lesson, we saw how to handle exceptions using a controller advice. But it won’t handle exceptions thrown in filters. How to handle those?
Remember our discussion in the last lesson on how Spring Boot handles exceptions by default? To summarize, Spring Boot maps the servlet error page to /error, and provides a BasicErrorController there. In other words, when an exception occurs, your request would be forwarded to the BasicErrorController. See Spring Boot documentation for more details.
Spring Boot allows us to replace that controller, or simply the DefaultErrorAttributes that it uses to build the response. So, our solution lies there! We can simply provide our custom ErrorController and ErrorAttributes, overriding Spring Boot’s BasicErrorController and DefaultErrorAttributes. Our ErrorAttributes would use the same ErrorResponseComposer that we had discussed in earlier lesson. For concrete examples, see Spring Lemon’s LemonErrorController and LemonErrorAttributes.
So, this completes our broad architecture. Let’s dive into the exact steps, from the very beginning.
1) Decide on a standard response format
Decide on a standard response format. For example, Spring Lemon's format looks as below, aligning with the default format of Spring Boot:
2) Code DTO for the above format
For examples, see Spring Lemon's ErrorResponse.
3) Code handler components
Code handler components for each exception type you wish to handle, which would actually build responses in the format discussed above. No need to code for those types which Spring Boot by default handles to your satisfaction. Also, for your custom exceptions, see if using @ResponseStatus as we discussed in the last lesson suffices.
Better to have an abstract class - like this one - and extend the handlers from that. You can actually have a hierarchy in some cases, like AbstractExceptionHandler -> AbstractBadRequestExceptionHandler -> ConstraintViolationExceptionHandler.
If you looked at the AbstractExceptionHandler, you'd have seen the exceptionName field there. That tells which exception type the handler handles.
4) Code the ErrorResponseComposer
Code the ErrorResponseComposer, injecting into it a map of the above handlers. The injection code could look like this:
Overriding Exception Handlers
If you are coding a library, say for your microservices, you may like to provide some standard handlers in the library, and allow your microservices to replace that with a custom implementation if needed.
You can leverage Spring’s @Order annotation for this. Specifically, you can have the handler classes in the library annotated with @Order(Ordered.LOWEST_PRECEDENCE), and let the microservices annotate their handlers with @Order(Ordered.HIGHEST_PRECEDENCE). Then, while injecting the handlers in the ErrorResponseComposer, you could filter out low precedence handlers. Here is that injection code, taken from Spring Lemon’s ErrorResponseComposer:
Actually, instead of coding all this yourself, you can use the spring-lemon-exceptions library, which comes with all the classes we discussed so far, and a few handlers. To do so, include the following dependency in your project:
5) Code a ControllerAdvice
Code a controller advice as discussed in the earlier lesson. When an exception is caught, it’ll
- Ask the above ErrorResponseComposer to compose a response. The composer would return an empty value if you wouldn't have coded a handler for the given exception type, in which case, just re-throw the exception, so that Spring Boot’s default mechanism (remember?) handles it.
- Re-throw the exception even if the composer returns a value, but either status or message is null. This will happen if you’ve coded a handler which doesn’t provide the status or message, leaving these to Spring Boot’s default mechanism.
- Return the composed response.
6) Code custom ErrorAttribute and ErrorController
Code custom ErrorAttributes and ErrorController, extending Spring Boot’s DefaultErrorAttributes and BasicErrorController, which try to get the message, status and errors by using our ErrorResponseComposer. In case anything is null, use defaults from the superclass.
So, this concludes our pattern! In the next lesson, we'll dive into validating request data on top of this architecture.
You may have a question: "With the presence of the custom ErrorController, isn't our controller advice redundant? Why not let every error just bubble up to the custom ErrorController?" Well, most of the exceptions will be related to validation or something that will come from the controllers rather then the filters, and forwarding those to /error would be a bit inefficient. Why not just have a tiny controller advice instead?