Managing exceptions in WebFlux using functional endpoint

Managing exceptions in WebFlux using functional endpoint

Working with web flux

I have been working a lot with Webflux for the past 6 months, I have designed new services for my customers, and am always striving to deliver the best.

For most of my applications, I like to use the functional endpoint instead of the classic RestController decorator. I can manipulate directly the server response and I feel more in control of what I am doing. The whole thing feels more concise, explicit and easy to document.

Using functional endpoints also means I have to manage all the exceptions that might be thrown from my app. Using the functionals endpoint, I can't use the classic RestControllerAdvice to manage those exceptions. I have also seen in some blog posts that using it is a "bad practice".

I needed to find a way to manage exceptions properly for the services I am designing.

I came across multiple articles advising on extending AbstractErrorWebExceptionHandler which does not sound the right way to do it.

How to manage the exceptions?

First, let's build a classic WebFlux app using SpringInitializr.

I am creating an interface that we can call Error that will describe the way of returning the right error content to the client.

public interface Error {

    Mono<ServerResponse> getAsServerResponse();
}

Then I will Build my Exception Type, let's say I want Business Exception and Technical Exception.

I will create them extending RuntimeException and implementing Error

I will give them a constructor, and a function to build a response body associated to the type of error.

public class BusinessException extends RuntimeException implements Error {

    private final Integer code;
    private final String message;
    private final String advice;

    public BusinessException(Integer code, String message, String advice) {
        super(message);
        this.code = code;
        this.message = message;
        this.advice = advice;
    }

    @Override
    public Mono<ServerResponse> getAsServerResponse() {
        return ServerResponse.badRequest().bodyValue(this.createBusinessErrorMessage());
    }

    protected BusinessError createBusinessErrorMessage() {
        return BusinessError.builder()
                .message(this.message)
                .advice(this.advice)
                .code(this.code)
                .build();
    }

}

The Technical can be created the same way.

Now If I wanna create a new exception, I would just have to extend either TechnicalException or BusinessException. The getAsServerResponse can stay the same or I can override it individually depending on the response code I want to return.

Testing the error management.

    @Bean
    RouterFunction<ServerResponse> getEmployeeByIdRoute() {
        return route(GET("/hello-world/{name}"),
                helloController::execute);
    }

I create a simple endpoint in which I will throw an error.

And here is how I manage everything at the same time.

 return Mono
                .fromSupplier(() -> new DummyReturn(request.pathVariable("name")))
                .flatMap( dummyReturn -> ServerResponse.ok().bodyValue(dummyReturn))

                .onErrorResume(BusinessException.class, BusinessException::getAsServerResponse)
                .onErrorResume(TechnicalException.class, TechnicalException::getAsServerResponse);
    }

Now I will just throw an error depending on random/fixed parameters

public DummyReturn(String name) {
        this.name = name;
        this.id = random.nextInt(20);
        if (Objects.equals(name, "error")) {
            throw new BusinessException(100, "Error in DummyReturn constructor", "Enter a correct name");
        }
        if (this.id > 10) {
            throw new TechnicalException("An error happened while attributing the ID");
        }
    }

And here is the result

GET http://localhost:8080/hello-world/df
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 20

{
  "id": 2,
  "name": "df"
}

GET http://localhost:8080/hello-world/df
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Content-Length: 56

{
  "message": "An error happened while attributing the ID"
}

I love this way cause we can be very explicit without having to write a ton of code.

If you are interested in seeing more of this, the entire codebase can be found on my GitHub at https://github.com/mathias-vandaele/webflux-error-management-functional-endpoint

Thank you for reading!