Spring WebFlux and domain exceptions
Recently I was assigned a task to implement an exception handling mechanism in a SpringBoot-based web application. It immediately turned out — as with almost everything with SpringBoot projects — that there’s a number of different ways to achieve it. Below is the outcome of my research and the final solution.
The requirements
Let’s start with collecting the requirements that the solution is obliged to fulfil.
#1 The returned error message must be compatible with the format provided by the framework, which — in JSON particularly — would be:
{
"timestamp": 1539797674906,
"path": "/",
"status": 418,
"error": "I'm a teapot",
"message": "Teapot Mike!"
}
This compatibility is important for a number of reasons.
First of all — an API consumer is always presented with the same error format.
Second, I’m still able to use the JSR-303/JSR-348 Bean Validation mechanism — leaving aside if this is a good idea or not for input validation — as a side note, I think that input validation should happen in the domain itself, as close to the model as possible. Nevertheless, having some @NotNull
annotated fields in the body will allow us to reject the incoming request without further processing. If validation errors happen, there’s an additional field added to the response — errors
which is an object reflecting BindingResult
.
Thirdly, there’s a trend nowadays to keep the application code as separated from the framework as possible. While I’m for that, I’m also aware of the fact that my application is steeped in the framework — in this situation I find using its mechanism reasonable.
#2 All exceptions thrown in the application should be automatically translated to corresponding HTTP status codes. One may think, that I want to introduce exception-based communication to the application. Definitely not. I’d like all the exceptions to be resolved no sooner than in the view layer (@Controller
) and passed there from facades wrapped into vavr’sTry
or Either
monadic containers.
#3 All the exceptions should extend from one base class (DomainException
) which in turn extends RuntimeException
— no place for checked fun here!
#4 There should be a single, centralized point where all the exceptions are caught and processed. Additionally it would be great if such an exception handler could be injected transparently into the application.
#5 I want to add an additional field to the error response — traceId
. In an microservice-based application it makes debugging much less problematic.
What we’ve got here
To handle an exception in a Spring Webflux based application you can:
- Use local
@ExceptionHandler
, extendResponseStatusException
or annotate the exception class with@ResponseStatus
annotation. - Provide a custom class annotated with
@ControllerAdvice
. - Provide a custom class that extends
DefaultErrorAttributes
.
There are examples with a handful of tests prepared for option 1, 2 and 3 respectively.
Option #1
Using @ResponseStatus
to annotate exception classes doesn’t seem to work well. First of all you need to add — and order properly — an additional configuration that provides WebExceptionHandler
. Unfortunately, adding this component spoils the exception handler chain and the body of the annotated exception is not available in the response at all. This breaks requirement #1 (not to mention #5 — traceId
). #3 is also broken, since no common hierarchy is present here.
Extending ResponseStatusException
seems to fulfil all the requirements. Also, by adding DomainException
which introduces another level of abstraction, ResponseStatusException
may be hidden in the extension hierarchy. The only problem is that traceId
(#5) may be passed only via constructor. This forces us to inject ReqTracer
into every class that throws an exception — which seems to be just another redundant dependency. What’s more, this field won’t be automatically added to the response body without interfering with Spring internals.
@ExceptionHandler
seems to be the way to go. It’s flexible — multiple objects can be passed as arguments to method and used to construct the response. The problem of @ExceptionHandler
is that it’s local only. That is, it can be used in the controller only, hence every single controller should have its own handler. Also, the response constructed within exception handler’s body is implemented in the application itself. It would be much better to use Spring’s class, however no such class exists(!).
Option #2
The second option seems very promising. There’s an injectable, centralized point for handling all the exceptions. ReqTracer
is injected into a single place only. All exceptions have the required fields in the body, all are part of the same hierarchy. Perfect? Nope.
Option #1 issue with the application-defined error response occurs here as well. What’s more it introduces inconsistency. To add traceId
field to the response we need to catch every exception that is thrown within the application. If so, for WebExchangeBindException
(which is thrown on failed input validation) we lose the errors
field in the response — or we need to add (and rewrite) it to the ErrorResponse
class. It means the Spring’s logic will be duplicated.
Option #3
Time for DefaultErrorAttributes
. It seems that this is what we’re looking for. By extending this class we fulfil all the requirements:
#1 Format compatibility — as I said earlier, there’s no class that represents error response in Spring Framework, it’s just a Map
. Here, we need to stick with the contract just by manipulating keys and values. The if
block ensures us that only domain exceptions will be additionally processed — so bean validation works like a charm.
For #2 and #3 we have an extendable DomainException
— all good!
#4 DomainExceptionWrapper
is a single, injectable component that may be published as an artifact to maven repository and added as a dependency to other applications. It’ll be automatically detected and injected as a bean while context is booting up. It’s also easily testable.
#5 Since all (domain, runtime, Spring) exceptions are handled in a single place and plain old Map
is used as an object being returned it’s very easy to add or remove fields from it.
Summing up
From all available options, #3 was finally chosen to implement exception handling in the project. Not only it fulfils all the requirements that were defined at the very beginning, but it can be easily extended in case of any (even unknown) upcoming changes. However, not in all cases there’s a need for such quite sophisticated mechanism. Simplicity first, so if any other of the suggested options helps you to achieve to the goal don’t hesitate to use it.
Looking for Scala and Java Experts?
We will make technology work for your business. See the projects we have successfully delivered.