Spring WebFlux Rest API Global Exception Handling

Spring WebFlux is part of Spring Framework, allowing us to Reactive programming and Support non-blocking I/O operations. The Spring Framework provides a lot of Annotations to handle the applications. This article focuses on Global Exception Handling by using Rest API in the Spring WebFlux. For this, we have one Spring Annotation that is @ControllerAdvice This annotation catches the exceptions from the entire module. Also, we can change the type of HTTP status code while dealing with the server by using this @ControllerAdvice annotation.

To achieve the Global exception handling, we need to create a custom Exception Handler class in the project. This class can handle the exceptions globally. Here we use ResponseStatusExceptionHandler interface to override the exception-handling methods with our logic.

Key Features Global Exception Handling:

  1. Centralized Error Management
    • Allows you to manage all exceptions in a single place rather than scattering exception-handling logic throughout your application code.
  2. Consistent Response Format
    • Ensures that all error responses follow a consistent format which improves the API’s usability and predictability for clients.
  3. Custom Exception Handling
    • Supports custom exceptions, allowing you to handle specific scenarios like ResourceNotFoundException, BadRequestException.
  4. Default Exception Handling
    • Provides a default handler for unexpected exceptions ensuring that the application does not expose internal details or stack traces in production.
  5. Integration with Reactive Programming
    • Non-blocking and efficient error handling in a reactive web environment.

Prerequisites:

  • Spring Framework
  • Spring WebFlux
  • Error Handling in Spring WebFlux
  • Spring Annotations
  • Components & Beans in Spring Framework
  • Java Programming

Tools & Technologies:

  • Spring Tool Suite
  • Spring Framework
  • Reactive programming
  • Maven

Steps to Implement Spring WebFlux Rest API Global Exception Handling

Here we created one simple spring reactive project by using spring initializr. It can able to create Spring Stater Project by using required dependencies. Below we provide the dependencies which are used in this Spring Application.

Dependencies:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>


Project Structure:

Below is the project structure, after creating the project using Spring Initializr.


Error Handling Responses

  1. ResourceNotFoundException:
    • Status: 404 Not Found
    • Message: Resource not found with id: {id}
  2. BadRequestException:
    • Status: 400 Bad Request
    • Message: Invalid request to create resource
  3. Generic Exception:
    • Status: 500 Internal Server Error
    • Message: An unexpected error occurred: {error message}


Steps 1:

Once project is created with required dependencies, then we created two custom exception classes, which can handle Runtime Exceptions. And we define constructors also in those two custom exception classes to provide meaning full error message to end user while getting error.

ResourceNotFoundException.java:

Java
package com.app;

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}


BadRequestExceptionjava:

Java
package com.app;

public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }
}

These two classes are to used to provide meaning full error message to the end user while handling exceptions globally in the Spring Application.


Steps 2:

After that, we created one java class for handling exceptions globally by using @ControllerAdvice spring annotations.

  • This class contains information about custom exception classes.
  • And here we use @ExceptionHandler annotation It is root exception handler in Spring framework.
  • For this annotation we provide custom exception classes as target classes.
  • Here we give two different custom exception classes namely ResourceNotFoundException and BadRequestException as target custom exception class to the @ExceptionHandler.
Java
package com.app;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.handler.ResponseStatusExceptionHandler;
import reactor.core.publisher.Mono;

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseStatusExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public Mono<ResponseEntity<String>> handleResourceNotFoundException(ResourceNotFoundException ex, ServerWebExchange exchange) {
        return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()));
    }

    @ExceptionHandler(BadRequestException.class)
    public Mono<ResponseEntity<String>> handleBadRequestException(BadRequestException ex, ServerWebExchange exchange) {
        return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public Mono<ResponseEntity<String>> handleGenericException(Exception ex, ServerWebExchange exchange) {
        return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred: " + ex.getMessage()));
    }
}


In the above class, we handle three different exceptions and converted into required HTTP Status code.

  • ResourceNotFoundException – NOT_FOUND
  • BadRequestException – BAD_REQUEST
  • Exception – INTERNAL_SERVER_ERROR

Steps 3:

Once required logic is completed to handle exceptions globally, Immediately we write logic for creating API endpoints and Router Functions in same same java class by using @Configuration Spring Annotation. For creating Routing end point in WebFlux we use RouterFunction interface in Spring Reactive programming. And This function returns a router.

RouterConfig.java:

Java
package com.app;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> routeFunction() {
        return RouterFunctions
                .route(RequestPredicates.GET("/resource/{id}"), this::getResource)
                .andRoute(RequestPredicates.POST("/resource"), this::createResource)
                ;
    }

    public Mono<ServerResponse> getResource(ServerRequest request) {
        String id = request.pathVariable("id");
        return Mono.error(new ResourceNotFoundException("Resource not found with id: " + id));
    }

    public Mono<ServerResponse> createResource(ServerRequest request) {
        return Mono.error(new BadRequestException("Invalid request to create resource"));
    }

    private RouterFunction<ServerResponse> errorRoutes() {
        return route()
                .onError(ResourceNotFoundException.class, this::handleResourceNotFoundException)
                .onError(BadRequestException.class, this::handleBadRequestException)
                .onError(Exception.class, this::handleGenericException)
                .build();
    }

    private Mono<ServerResponse> handleResourceNotFoundException(Throwable throwable, ServerRequest request) {
        return ServerResponse.status(HttpStatus.NOT_FOUND)
                .contentType(MediaType.TEXT_PLAIN)
                .bodyValue(throwable.getMessage());
    }

    private Mono<ServerResponse> handleBadRequestException(Throwable throwable, ServerRequest request) {
        return ServerResponse.status(HttpStatus.BAD_REQUEST)
                .contentType(MediaType.TEXT_PLAIN)
                .bodyValue(throwable.getMessage());
    }

    private Mono<ServerResponse> handleGenericException(Throwable throwable, ServerRequest request) {
        return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .contentType(MediaType.TEXT_PLAIN)
                .bodyValue("An unexpected error occurred: " + throwable.getMessage());
    }
}


In this above class, first we define the Routing End points.

@Bean
public RouterFunction<ServerResponse> routeFunction() {
return RouterFunctions
.route(RequestPredicates.GET("/resource/{id}"), this::getResource)
.andRoute(RequestPredicates.POST("/resource"), this::createResource)
;
}


After this, we created two API endpoints by using GET and POST HTTP request type.

public Mono<ServerResponse> getResource(ServerRequest request) {
String id = request.pathVariable("id");
return Mono.error(new ResourceNotFoundException("Resource not found with id: " + id));
}

public Mono<ServerResponse> createResource(ServerRequest request) {
return Mono.error(new BadRequestException("Invalid request to create resource"));
}


After this, again we created one more Router Function to handle the Exceptions.

private RouterFunction<ServerResponse> errorRoutes() {
return route()
.onError(ResourceNotFoundException.class, this::handleResourceNotFoundException)
.onError(BadRequestException.class, this::handleBadRequestException)
.onError(Exception.class, this::handleGenericException)
.build();
}


After this we created Mono type publishers to to customize the exceptions types into required HTTP codes like 403, 400 and other code.

private Mono<ServerResponse> handleResourceNotFoundException(Throwable throwable, ServerRequest request) {
return ServerResponse.status(HttpStatus.NOT_FOUND)
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(throwable.getMessage());
}

private Mono<ServerResponse> handleBadRequestException(Throwable throwable, ServerRequest request) {
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(throwable.getMessage());
}

private Mono<ServerResponse> handleGenericException(Throwable throwable, ServerRequest request) {
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.TEXT_PLAIN)
.bodyValue("An unexpected error occurred: " + throwable.getMessage());
}


Steps 4: Run the Application

Now required logic is completed. Now Run this project as Spring Boot App and this project runs on Netty Server with port number 8080 by default.


Steps 5: Testing the APIs

After running the project, we need test the API endpoints, here we use Post Man tool for testing API endpoints.

getResource API:

http://localhost:8080/resources/101


createResource API:

http://localhost:8080/resources


Advantages of Global exception handling

  • Improved Maintainability
  • Enhanced Debugging and Logging
  • Better Client Communication
  • Separation of Concerns
  • Scalability


Contact Us