Spring WebFlux Rest API Global exception handling

The purpose of this post is to explain how to implement custom exception handling with Spring WebFlux for a REST API.

Error handling is one of the ways we make sure we handle our failures consistently. The benefit of handling exceptions globally is that we can customize all error output from our API into a single format that all consumers can understand. This will guarantee us easier and faster maintenance.

Prerequisites

This is the list of all the prerequisites for following this story:

  • Java 17
  • Spring Boot / Starter WebFlux 2.6.7
  • Lombok 1.18
  • Maven 3.6.3
  • Postman

Getting Started

First, let’s create a sample spring reactive API from start.spring.io. In our project, we will expose RESTful APIs with @RestController and RouterFunction.

@RestController
@RequestMapping("/api")
public class BookController {

    @GetMapping("/book")
    public Mono<ResponseEntity<Book>> getBookById() {
        return Mono.error(new UnAuthorizedException("Access denied"));
    }

}
@Component
public class AuthorHandler {

    public Mono<ServerResponse> getAuthor(ServerRequest serverRequest) {
        return Mono.error(new UnAuthorizedException("Access denied"));
    }
}

@Configuration
@AllArgsConstructor
public class Router {

    @Bean
    public RouterFunction<ServerResponse> routeUserAccount(final AuthorHandler authorHandler) {
        return route()
                .nest(path("/api"), builder -> builder.GET("/author", authorHandler::getAuthor))
                .build();
    }
}

UnAuthorizedException.java: This is a user-defined class extending the java.lang.RuntimeException class.

When we test the http://localhost:8080/api/book endpoint, it will throw a default internal error exception.

Global Error Handling

The custom exception system provided in SpringMVC doesn’t work in Spring-WebFlux for the simple reason that the underlying runtime containers are not the same. WebExceptionHandler is the top-level interface to the exception handler of Spring-WebFlux, so it can be traced back to the subclass.

Spring Boot provides a WebExceptionHandler that sensibly handles all errors. Its position in the processing order is immediately before the handlers provided by WebFlux, which are considered last.

We need two steps to create our custom exception:

  1. Customize the Global Error Response Attributes
  2. Implement the Global Error Handler

The first step in customizing is to override the getErrorAttributes method of the parent default error attribute class DefaultErrorAttributes to get the error attribute and catch the corresponding exception from the service request encapsulation ServerRequest.

record ExceptionRule(Class<?> exceptionClass, HttpStatus status){}

@Component
public class GlobalErrorAttributes extends DefaultErrorAttributes {

    private final List<ExceptionRule> exceptionsRules = List.of(
            new ExceptionRule(UnAuthorizedException.class, HttpStatus.UNAUTHORIZED)
    );


    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        Throwable error = getError(request);

        final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
        Optional<ExceptionRule> exceptionRuleOptional = exceptionsRules.stream()
                .map(exceptionRule -> exceptionRule.exceptionClass().isInstance(error) ? exceptionRule : null)
                .filter(Objects::nonNull)
                .findFirst();

       return exceptionRuleOptional.<Map<String, Object>>map(exceptionRule -> Map.of(ErrorAttributesKey.CODE.getKey(), exceptionRule.status().value(), ErrorAttributesKey.MESSAGE.getKey(), error.getMessage(),  ErrorAttributesKey.TIME.getKey(), timestamp))
               .orElseGet(() -> Map.of(ErrorAttributesKey.CODE.getKey(), determineHttpStatus(error).value(),  ErrorAttributesKey.MESSAGE.getKey(), error.getMessage(), ErrorAttributesKey.TIME.getKey(), timestamp));
    }


    private HttpStatus determineHttpStatus(Throwable error) {
        return error instanceof ResponseStatusException err ? err.getStatus() : MergedAnnotations.from(error.getClass(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class).getValue(ErrorAttributesKey.CODE.getKey(), HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
    }

}

Here we defined a standard error JSON response with codemessage, and timestamp. exceptionsRules is an ExceptionRule record list that contains all user-defined exception classes with the desired error status.

Now, let’s implement the Global Error Handler.

@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    public GlobalErrorWebExceptionHandler(GlobalErrorAttributes globalErrorAttributes, ApplicationContext applicationContext,
                                          ServerCodecConfigurer serverCodecConfigurer) {
        super(globalErrorAttributes, new WebProperties.Resources(), applicationContext);
        super.setMessageWriters(serverCodecConfigurer.getWriters());
        super.setMessageReaders(serverCodecConfigurer.getReaders());
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {

        final Map<String, Object> errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());

        int statusCode = Integer.parseInt(errorPropertiesMap.get(ErrorAttributesKey.CODE.getKey()).toString());
        return ServerResponse.status(HttpStatus.valueOf(statusCode))
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(errorPropertiesMap));
    }
}

This class extends the AbstractErrorWebExceptionHandler class provided by the spring and custom implements the handling of the Global Exception. @Order(-2) is used to give a priority boost to the component that the Spring Boot class DefaultErrorWebExceptionHandler which is of Order(-1) priority.

Run the application and test the API using POSTMAN.

It can be seen from the figures above that the received JSON response is uniform for all detected errors.

Conclusion

Well done !!. In this story, We have seen the customized exception handling using Spring Webflux.

The complete source code is available on GitHub.

References

👉 Link to Medium blog

Related Posts