Spring WebFlux and domain exceptions

Maciek Opała
SoftwareMill Tech Blog
5 min readNov 14, 2018

--

Photo credit: rawpixel.com

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:

  1. Use local @ExceptionHandler, extend ResponseStatusException or annotate the exception class with @ResponseStatus annotation.
  2. Provide a custom class annotated with@ControllerAdvice.
  3. 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?

Contact us!

We will make technology work for your business. See the projects we have successfully delivered.

--

--