or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

annotation-controllers.mdconfiguration.mdcontent-negotiation.mdexception-handling.mdfunctional-routing.mdindex.mdresource-handling.mdview-rendering.mdwebclient.mdwebsocket.md
tile.json

exception-handling.mddocs/

Exception Handling

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.

Capabilities

Dispatch Exception Handler

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);
    }
}

Response Entity Exception Handler

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...
}

Exception Handler Methods

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());
    }
}

Response Status Exception Handler

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)
            ));
    }
}

Functional Routing Exception Handling

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();
    }
}

Handler Adapter Exception Handling

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);
    }
}

Types

Common Exception Handling Patterns

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;
}

Testing Exception Handlers

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();
    }
}