Spring WebFlux provides comprehensive exception handling mechanisms for both annotation-based controllers and functional routing. The framework includes built-in exception handlers for common scenarios and allows custom exception handling through @ExceptionHandler methods and DispatchExceptionHandler implementations.
The DispatchExceptionHandler interface handles exceptions during request dispatch.
public interface DispatchExceptionHandler {
// Handle the exception and return a HandlerResult
Mono<HandlerResult> handleError(ServerWebExchange exchange, Throwable ex);
}Usage:
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.DispatchExceptionHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.reactive.HandlerResult;
import reactor.core.publisher.Mono;
@Component
public class CustomDispatchExceptionHandler implements DispatchExceptionHandler {
@Override
public Mono<HandlerResult> handleError(ServerWebExchange exchange, Throwable ex) {
if (ex instanceof NotFoundException) {
ErrorResponse error = new ErrorResponse("Not Found", ex.getMessage());
return Mono.just(new HandlerResult(this, error,
ResolvableType.forInstance(error)));
}
return Mono.error(ex);
}
}The ResponseEntityExceptionHandler is a base class for @ControllerAdvice that handles exceptions and returns ResponseEntity.
@ControllerAdvice
public abstract class ResponseEntityExceptionHandler {
// Handle all Spring WebFlux exceptions
@ExceptionHandler({
ServerWebInputException.class,
ResponseStatusException.class,
MethodNotAllowedException.class,
NotAcceptableStatusException.class,
UnsupportedMediaTypeStatusException.class,
ServerErrorException.class
})
public final Mono<ResponseEntity<Object>> handleException(Exception ex, ServerWebExchange exchange) { ... }
// Handle ServerWebInputException
protected Mono<ResponseEntity<Object>> handleServerWebInputException(
ServerWebInputException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Handle MethodNotAllowedException
protected Mono<ResponseEntity<Object>> handleMethodNotAllowedException(
MethodNotAllowedException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Handle NotAcceptableStatusException
protected Mono<ResponseEntity<Object>> handleNotAcceptableStatusException(
NotAcceptableStatusException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Handle UnsupportedMediaTypeStatusException
protected Mono<ResponseEntity<Object>> handleUnsupportedMediaTypeStatusException(
UnsupportedMediaTypeStatusException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Handle ResponseStatusException
protected Mono<ResponseEntity<Object>> handleResponseStatusException(
ResponseStatusException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Handle ServerErrorException
protected Mono<ResponseEntity<Object>> handleServerErrorException(
ServerErrorException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Create response entity
protected Mono<ResponseEntity<Object>> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
// Create error response body
protected Mono<Object> createResponseBody(
Exception ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) { ... }
}Usage:
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Handle custom exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public Mono<ResponseEntity<Object>> handleResourceNotFound(
ResourceNotFoundException ex,
ServerWebExchange exchange) {
ErrorResponse body = new ErrorResponse(
"RESOURCE_NOT_FOUND",
ex.getMessage(),
exchange.getRequest().getPath().value()
);
return Mono.just(ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(body));
}
@ExceptionHandler(ValidationException.class)
public Mono<ResponseEntity<Object>> handleValidation(
ValidationException ex,
ServerWebExchange exchange) {
ErrorResponse body = new ErrorResponse(
"VALIDATION_ERROR",
ex.getMessage(),
ex.getErrors()
);
return Mono.just(ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(body));
}
// Override to customize handling of Spring WebFlux exceptions
@Override
protected Mono<ResponseEntity<Object>> handleServerWebInputException(
ServerWebInputException ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) {
ErrorResponse body = new ErrorResponse(
"INVALID_INPUT",
"Invalid request input: " + ex.getReason(),
exchange.getRequest().getPath().value()
);
return handleExceptionInternal(ex, body, headers, status, exchange);
}
// Customize error response body
@Override
protected Mono<Object> createResponseBody(
Exception ex,
HttpHeaders headers,
HttpStatusCode status,
ServerWebExchange exchange) {
return Mono.just(new ErrorResponse(
status.toString(),
ex.getMessage(),
exchange.getRequest().getPath().value()
));
}
}
// Error response model
public class ErrorResponse {
private String code;
private String message;
private String path;
private Instant timestamp;
private Object details;
public ErrorResponse(String code, String message, String path) {
this.code = code;
this.message = message;
this.path = path;
this.timestamp = Instant.now();
}
// Getters and setters...
}Use @ExceptionHandler annotation in controllers or @ControllerAdvice classes to handle specific exceptions.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
// Exception types handled by this method
Class<? extends Throwable>[] value() default {};
}Usage:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id);
}
// Handle exceptions within this controller
@ExceptionHandler(UserNotFoundException.class)
public Mono<ResponseEntity<ErrorResponse>> handleUserNotFound(
UserNotFoundException ex,
ServerWebExchange exchange) {
ErrorResponse error = new ErrorResponse(
"USER_NOT_FOUND",
ex.getMessage()
);
return Mono.just(ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(error));
}
@ExceptionHandler(IllegalArgumentException.class)
public Mono<ResponseEntity<ErrorResponse>> handleIllegalArgument(
IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
"INVALID_ARGUMENT",
ex.getMessage()
);
return Mono.just(ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(error));
}
}
// Global exception handlers
@ControllerAdvice
public class GlobalControllerAdvice {
// Handle multiple exception types
@ExceptionHandler({
AccessDeniedException.class,
UnauthorizedException.class
})
public Mono<ResponseEntity<ErrorResponse>> handleAccessDenied(Exception ex) {
ErrorResponse error = new ErrorResponse(
"ACCESS_DENIED",
"You don't have permission to access this resource"
);
return Mono.just(ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(error));
}
// Handle all uncaught exceptions
@ExceptionHandler(Exception.class)
public Mono<ResponseEntity<ErrorResponse>> handleGeneral(
Exception ex,
ServerWebExchange exchange) {
logger.error("Unhandled exception", ex);
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred"
);
return Mono.just(ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(error));
}
// Return different response types
@ExceptionHandler(DataIntegrityException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public Mono<String> handleDataIntegrity(DataIntegrityException ex) {
return Mono.just("Data conflict: " + ex.getMessage());
}
}The WebFluxResponseStatusExceptionHandler handles exceptions annotated with @ResponseStatus.
public class WebFluxResponseStatusExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { ... }
}Usage with @ResponseStatus:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
// Exception with response status
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidRequestException extends RuntimeException {
public InvalidRequestException(String message) {
super(message);
}
}
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Access denied")
public class AccessDeniedException extends RuntimeException {
public AccessDeniedException() {
super("Access denied");
}
}
// Use in controller
@RestController
public class ProductController {
@GetMapping("/products/{id}")
public Mono<Product> getProduct(@PathVariable String id) {
return productService.findById(id)
.switchIfEmpty(Mono.error(
new ResourceNotFoundException("Product not found: " + id)
));
}
}Handle exceptions in functional routes using filters and error handlers.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class FunctionalRoutes {
@Bean
public RouterFunction<ServerResponse> userRoutes(UserHandler handler) {
return route()
.GET("/users/{id}", handler::getUser)
.POST("/users", handler::createUser)
// Add error handling filter
.onError(NotFoundException.class, (ex, request) ->
ServerResponse.status(HttpStatus.NOT_FOUND)
.bodyValue(new ErrorResponse("NOT_FOUND", ex.getMessage()))
)
.onError(ValidationException.class, (ex, request) ->
ServerResponse.status(HttpStatus.BAD_REQUEST)
.bodyValue(new ErrorResponse("VALIDATION_ERROR", ex.getMessage()))
)
// Catch all other exceptions
.onError(Exception.class, (ex, request) -> {
logger.error("Error handling request", ex);
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue(new ErrorResponse("INTERNAL_ERROR", "An error occurred"));
})
.build();
}
// Using filter for exception handling
@Bean
public RouterFunction<ServerResponse> apiRoutes(ApiHandler handler) {
HandlerFilterFunction<ServerResponse, ServerResponse> errorHandler =
(request, next) -> next.handle(request)
.onErrorResume(NotFoundException.class, ex ->
ServerResponse.status(HttpStatus.NOT_FOUND)
.bodyValue(Map.of(
"error", "Not Found",
"message", ex.getMessage(),
"path", request.path()
))
)
.onErrorResume(Exception.class, ex -> {
logger.error("Error in API handler", ex);
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue(Map.of(
"error", "Internal Server Error",
"message", "An unexpected error occurred"
));
});
return route()
.path("/api", builder -> builder
.GET("/data", handler::getData)
.POST("/data", handler::createData)
.filter(errorHandler))
.build();
}
}The HandlerAdapter interface can optionally implement DispatchExceptionHandler for exception handling during handler execution.
import org.springframework.web.reactive.HandlerAdapter;
import org.springframework.web.reactive.DispatchExceptionHandler;
public class CustomHandlerAdapter implements HandlerAdapter, DispatchExceptionHandler {
@Override
public boolean supports(Object handler) {
return handler instanceof CustomHandler;
}
@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
return ((CustomHandler) handler).handle(exchange);
}
@Override
public Mono<HandlerResult> handleError(ServerWebExchange exchange, Throwable ex) {
// Handle exceptions from this adapter
if (ex instanceof CustomException) {
ErrorResponse error = new ErrorResponse("CUSTOM_ERROR", ex.getMessage());
return Mono.just(new HandlerResult(this, error,
ResolvableType.forInstance(error)));
}
return Mono.error(ex);
}
}Complete examples for common exception handling scenarios:
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.bind.support.WebExchangeBindException;
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import java.util.stream.Collectors;
@ControllerAdvice
public class ComprehensiveExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(ComprehensiveExceptionHandler.class);
// Handle validation errors
@ExceptionHandler(WebExchangeBindException.class)
public Mono<ResponseEntity<ValidationErrorResponse>> handleValidation(
WebExchangeBindException ex,
ServerWebExchange exchange) {
List<FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldError(
error.getField(),
error.getDefaultMessage(),
error.getRejectedValue()
))
.collect(Collectors.toList());
ValidationErrorResponse response = new ValidationErrorResponse(
"VALIDATION_ERROR",
"Request validation failed",
fieldErrors,
exchange.getRequest().getPath().value()
);
return Mono.just(ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(response));
}
// Handle resource not found
@ExceptionHandler(ResourceNotFoundException.class)
public Mono<ResponseEntity<ErrorResponse>> handleResourceNotFound(
ResourceNotFoundException ex,
ServerWebExchange exchange) {
ErrorResponse response = ErrorResponse.builder()
.code("RESOURCE_NOT_FOUND")
.message(ex.getMessage())
.path(exchange.getRequest().getPath().value())
.timestamp(Instant.now())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response));
}
// Handle authentication errors
@ExceptionHandler(AuthenticationException.class)
public Mono<ResponseEntity<ErrorResponse>> handleAuthentication(
AuthenticationException ex,
ServerWebExchange exchange) {
ErrorResponse response = ErrorResponse.builder()
.code("AUTHENTICATION_FAILED")
.message("Authentication failed: " + ex.getMessage())
.path(exchange.getRequest().getPath().value())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, "Bearer")
.body(response));
}
// Handle authorization errors
@ExceptionHandler(AuthorizationException.class)
public Mono<ResponseEntity<ErrorResponse>> handleAuthorization(
AuthorizationException ex,
ServerWebExchange exchange) {
ErrorResponse response = ErrorResponse.builder()
.code("AUTHORIZATION_FAILED")
.message("You don't have permission to access this resource")
.path(exchange.getRequest().getPath().value())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(response));
}
// Handle rate limiting
@ExceptionHandler(RateLimitExceededException.class)
public Mono<ResponseEntity<ErrorResponse>> handleRateLimit(
RateLimitExceededException ex,
ServerWebExchange exchange) {
ErrorResponse response = ErrorResponse.builder()
.code("RATE_LIMIT_EXCEEDED")
.message("Rate limit exceeded. Please try again later.")
.path(exchange.getRequest().getPath().value())
.details(Map.of(
"retryAfter", ex.getRetryAfter(),
"limit", ex.getLimit()
))
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.header("Retry-After", String.valueOf(ex.getRetryAfter()))
.body(response));
}
// Handle service unavailable
@ExceptionHandler(ServiceUnavailableException.class)
public Mono<ResponseEntity<ErrorResponse>> handleServiceUnavailable(
ServiceUnavailableException ex,
ServerWebExchange exchange) {
ErrorResponse response = ErrorResponse.builder()
.code("SERVICE_UNAVAILABLE")
.message("Service temporarily unavailable: " + ex.getMessage())
.path(exchange.getRequest().getPath().value())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.header("Retry-After", "60")
.body(response));
}
// Handle database errors
@ExceptionHandler(DataAccessException.class)
public Mono<ResponseEntity<ErrorResponse>> handleDataAccess(
DataAccessException ex,
ServerWebExchange exchange) {
logger.error("Database error", ex);
ErrorResponse response = ErrorResponse.builder()
.code("DATABASE_ERROR")
.message("A database error occurred")
.path(exchange.getRequest().getPath().value())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response));
}
// Handle timeout errors
@ExceptionHandler(TimeoutException.class)
public Mono<ResponseEntity<ErrorResponse>> handleTimeout(
TimeoutException ex,
ServerWebExchange exchange) {
logger.warn("Request timeout", ex);
ErrorResponse response = ErrorResponse.builder()
.code("REQUEST_TIMEOUT")
.message("Request processing timed out")
.path(exchange.getRequest().getPath().value())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.REQUEST_TIMEOUT)
.body(response));
}
// Catch-all handler
@ExceptionHandler(Exception.class)
public Mono<ResponseEntity<ErrorResponse>> handleGeneral(
Exception ex,
ServerWebExchange exchange) {
logger.error("Unhandled exception", ex);
ErrorResponse response = ErrorResponse.builder()
.code("INTERNAL_ERROR")
.message("An unexpected error occurred")
.path(exchange.getRequest().getPath().value())
.build();
return Mono.just(ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response));
}
}
// Error response models
@Data
@Builder
public class ErrorResponse {
private String code;
private String message;
private String path;
private Instant timestamp = Instant.now();
private Object details;
}
@Data
public class ValidationErrorResponse extends ErrorResponse {
private List<FieldError> fieldErrors;
public ValidationErrorResponse(String code, String message,
List<FieldError> fieldErrors, String path) {
super.setCode(code);
super.setMessage(message);
super.setPath(path);
this.fieldErrors = fieldErrors;
}
}
@Data
@AllArgsConstructor
public class FieldError {
private String field;
private String message;
private Object rejectedValue;
}import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import static org.mockito.Mockito.when;
@WebFluxTest(controllers = UserController.class)
class ExceptionHandlerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private UserService userService;
@Test
void shouldReturn404WhenUserNotFound() {
when(userService.findById("123"))
.thenReturn(Mono.error(new ResourceNotFoundException("User not found")));
webTestClient.get()
.uri("/users/123")
.exchange()
.expectStatus().isNotFound()
.expectBody()
.jsonPath("$.code").isEqualTo("RESOURCE_NOT_FOUND")
.jsonPath("$.message").isEqualTo("User not found");
}
@Test
void shouldReturn400OnValidationError() {
User invalidUser = new User();
// Missing required fields
webTestClient.post()
.uri("/users")
.bodyValue(invalidUser)
.exchange()
.expectStatus().isBadRequest()
.expectBody()
.jsonPath("$.code").isEqualTo("VALIDATION_ERROR")
.jsonPath("$.fieldErrors").isArray();
}
}