CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-boot--spring-boot-starter-webflux

Starter for building WebFlux applications using Spring Framework's Reactive Web support

Pending
Overview
Eval results
Files

error-handling.mddocs/

Error Handling

Spring WebFlux provides comprehensive reactive error handling with customizable exception handlers, consistent error responses across different content types, and integration with Spring Boot's error handling infrastructure.

Core Error Handling Interfaces

ErrorWebExceptionHandler

public interface ErrorWebExceptionHandler extends WebExceptionHandler {
    Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
}

@FunctionalInterface
public interface WebExceptionHandler {
    Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
}

AbstractErrorWebExceptionHandler

public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExceptionHandler, ApplicationContextAware {
    
    public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes,
                                          WebProperties.Resources resources,
                                          ApplicationContext applicationContext);
    
    protected abstract RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes);
    
    protected void setMessageWriters(List<HttpMessageWriter<?>> messageWriters);
    protected void setMessageReaders(List<HttpMessageReader<?>> messageReaders);
    protected void setViewResolvers(List<ViewResolver> viewResolvers);
    
    protected ServerRequest createServerRequest(ServerWebExchange exchange, Throwable ex);
    protected void logError(ServerRequest request, ServerResponse response, Throwable throwable);
    
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
}

DefaultErrorWebExceptionHandler

public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
    
    public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes,
                                         WebProperties.Resources resources,
                                         ErrorProperties errorProperties,
                                         ApplicationContext applicationContext);
    
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes);
    
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request);
    protected Mono<ServerResponse> renderErrorView(ServerRequest request);
    protected int getHttpStatus(Map<String, Object> errorAttributes);
    protected Mono<String> renderDefaultErrorView(ServerRequest request);
}

Error Attributes

ErrorAttributes Interface

public interface ErrorAttributes {
    
    Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options);
    Throwable getError(ServerRequest request);
    void storeErrorInformation(Throwable error, ServerWebExchange exchange);
}

public class ErrorAttributeOptions {
    
    public static ErrorAttributeOptions defaults();
    public static ErrorAttributeOptions of(Include... includes);
    
    public ErrorAttributeOptions including(Include... includes);
    public ErrorAttributeOptions excluding(Include... excludes);
    
    public enum Include {
        EXCEPTION, STACK_TRACE, MESSAGE, BINDING_ERRORS
    }
}

DefaultErrorAttributes

public class DefaultErrorAttributes implements ErrorAttributes {
    
    private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
    
    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options);
    
    @Override
    public Throwable getError(ServerRequest request);
    
    @Override
    public void storeErrorInformation(Throwable error, ServerWebExchange exchange);
    
    protected String getTrace(Throwable ex);
    protected void addStackTrace(Map<String, Object> errorAttributes, Throwable ex);
    protected void addErrorMessage(Map<String, Object> errorAttributes, Throwable ex);
    protected void addBindingResults(Map<String, Object> errorAttributes, ServerRequest request);
}

Custom Error Handlers

Global Error Handler

@Component
@Order(-2) // Higher precedence than DefaultErrorWebExceptionHandler
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
    
    public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes,
                                        WebProperties.Resources resources,
                                        ApplicationContext applicationContext) {
        super(errorAttributes, resources, applicationContext);
    }
    
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }
    
    private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        Map<String, Object> errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());
        Throwable error = getError(request);
        
        return switch (error) {
            case ValidationException ve -> handleValidationError(request, ve);
            case ResourceNotFoundException rnfe -> handleResourceNotFound(request, rnfe);
            case AuthenticationException ae -> handleAuthenticationError(request, ae);
            case AuthorizationException aze -> handleAuthorizationError(request, aze);
            case BusinessException be -> handleBusinessError(request, be);
            default -> handleGenericError(request, errorPropertiesMap);
        };
    }
    
    private Mono<ServerResponse> handleValidationError(ServerRequest request, ValidationException ex) {
        ErrorResponse errorResponse = ErrorResponse.builder()
            .code("VALIDATION_ERROR")
            .message(ex.getMessage())
            .details(ex.getValidationErrors())
            .timestamp(Instant.now())
            .path(request.path())
            .build();
        
        return ServerResponse.badRequest()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> handleResourceNotFound(ServerRequest request, ResourceNotFoundException ex) {
        ErrorResponse errorResponse = ErrorResponse.builder()
            .code("RESOURCE_NOT_FOUND")
            .message(ex.getMessage())
            .timestamp(Instant.now())
            .path(request.path())
            .build();
        
        return ServerResponse.notFound()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> handleAuthenticationError(ServerRequest request, AuthenticationException ex) {
        ErrorResponse errorResponse = ErrorResponse.builder()
            .code("AUTHENTICATION_FAILED")
            .message("Authentication required")
            .timestamp(Instant.now())
            .path(request.path())
            .build();
        
        return ServerResponse.status(HttpStatus.UNAUTHORIZED)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> handleAuthorizationError(ServerRequest request, AuthorizationException ex) {
        ErrorResponse errorResponse = ErrorResponse.builder()
            .code("AUTHORIZATION_FAILED")
            .message("Insufficient permissions")
            .timestamp(Instant.now())
            .path(request.path())
            .build();
        
        return ServerResponse.status(HttpStatus.FORBIDDEN)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> handleBusinessError(ServerRequest request, BusinessException ex) {
        ErrorResponse errorResponse = ErrorResponse.builder()
            .code(ex.getErrorCode())
            .message(ex.getMessage())
            .timestamp(Instant.now())
            .path(request.path())
            .build();
        
        return ServerResponse.status(HttpStatus.UNPROCESSABLE_ENTITY)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> handleGenericError(ServerRequest request, Map<String, Object> errorAttributes) {
        int status = (int) errorAttributes.getOrDefault("status", 500);
        String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
        
        ErrorResponse errorResponse = ErrorResponse.builder()
            .code("INTERNAL_ERROR")
            .message(message)
            .timestamp(Instant.now())
            .path(request.path())
            .build();
        
        return ServerResponse.status(status)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
}

Controller-Level Error Handling

@RestController
@RequestMapping("/api/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)
            .switchIfEmpty(Mono.error(new UserNotFoundException("User not found: " + id)))
            .onErrorMap(DataAccessException.class, ex -> 
                new ServiceException("Database error while fetching user", ex));
    }
    
    @PostMapping
    public Mono<User> createUser(@Valid @RequestBody Mono<User> userMono) {
        return userMono
            .flatMap(userService::save)
            .onErrorMap(ConstraintViolationException.class, ex ->
                new ValidationException("User validation failed", ex))
            .onErrorMap(DataIntegrityViolationException.class, ex ->
                new BusinessException("USER_ALREADY_EXISTS", "User already exists"));
    }
    
    @PutMapping("/{id}")
    public Mono<User> updateUser(@PathVariable String id, @Valid @RequestBody Mono<User> userMono) {
        return userMono
            .flatMap(user -> userService.update(id, user))
            .switchIfEmpty(Mono.error(new UserNotFoundException("User not found: " + id)))
            .onErrorMap(OptimisticLockingFailureException.class, ex ->
                new BusinessException("CONCURRENT_MODIFICATION", "User was modified by another process"));
    }
    
    // Controller-specific exception handlers
    @ExceptionHandler(UserNotFoundException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleUserNotFound(UserNotFoundException ex) {
        ErrorResponse error = ErrorResponse.builder()
            .code("USER_NOT_FOUND")
            .message(ex.getMessage())
            .timestamp(Instant.now())
            .build();
        
        return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(error));
    }
    
    @ExceptionHandler(ValidationException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleValidation(ValidationException ex) {
        ErrorResponse error = ErrorResponse.builder()
            .code("VALIDATION_ERROR")
            .message(ex.getMessage())
            .details(ex.getValidationErrors())
            .timestamp(Instant.now())
            .build();
        
        return Mono.just(ResponseEntity.badRequest().body(error));
    }
}

Functional Error Handling

@Configuration
public class ErrorHandlingRouterConfiguration {
    
    @Bean
    public RouterFunction<ServerResponse> userRoutes(UserHandler userHandler) {
        return RouterFunctions.route()
            .GET("/api/users/{id}", userHandler::getUser)
            .POST("/api/users", userHandler::createUser)
            .onError(UserNotFoundException.class, this::handleUserNotFound)
            .onError(ValidationException.class, this::handleValidationError)
            .onError(Exception.class, this::handleGenericError)
            .build();
    }
    
    private Mono<ServerResponse> handleUserNotFound(Throwable ex, ServerRequest request) {
        ErrorResponse error = ErrorResponse.builder()
            .code("USER_NOT_FOUND")
            .message(ex.getMessage())
            .path(request.path())
            .timestamp(Instant.now())
            .build();
        
        return ServerResponse.notFound()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(error);
    }
    
    private Mono<ServerResponse> handleValidationError(Throwable ex, ServerRequest request) {
        ValidationException validationEx = (ValidationException) ex;
        ErrorResponse error = ErrorResponse.builder()
            .code("VALIDATION_ERROR")
            .message(validationEx.getMessage())
            .details(validationEx.getValidationErrors())
            .path(request.path())
            .timestamp(Instant.now())
            .build();
        
        return ServerResponse.badRequest()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(error);
    }
    
    private Mono<ServerResponse> handleGenericError(Throwable ex, ServerRequest request) {
        log.error("Unexpected error in request: {} {}", request.method(), request.path(), ex);
        
        ErrorResponse error = ErrorResponse.builder()
            .code("INTERNAL_ERROR")
            .message("An unexpected error occurred")
            .path(request.path())
            .timestamp(Instant.now())    
            .build();
        
        return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(error);
    }
}

Error Response Models

Standard Error Response

public class ErrorResponse {
    private final String code;
    private final String message;
    private final String path;
    private final Instant timestamp;
    private final Object details;
    
    public static Builder builder() {
        return new Builder();
    }
    
    public static class Builder {
        private String code;
        private String message;
        private String path;
        private Instant timestamp;
        private Object details;
        
        public Builder code(String code) {
            this.code = code;
            return this;
        }
        
        public Builder message(String message) {
            this.message = message;
            return this;
        }
        
        public Builder path(String path) {
            this.path = path;
            return this;
        }
        
        public Builder timestamp(Instant timestamp) {
            this.timestamp = timestamp;
            return this;
        }
        
        public Builder details(Object details) {
            this.details = details;
            return this;
        }
        
        public ErrorResponse build() {
            return new ErrorResponse(code, message, path, timestamp, details);
        }
    }
    
    // Getters...
}

Validation Error Details

public class ValidationErrorDetails {
    private final List<FieldError> fieldErrors;
    private final List<GlobalError> globalErrors;
    
    public ValidationErrorDetails(List<FieldError> fieldErrors, List<GlobalError> globalErrors) {
        this.fieldErrors = fieldErrors != null ? fieldErrors : Collections.emptyList();
        this.globalErrors = globalErrors != null ? globalErrors : Collections.emptyList();
    }
    
    public static class FieldError {
        private final String field;
        private final Object rejectedValue;
        private final String message;
        private final String code;
        
        public FieldError(String field, Object rejectedValue, String message, String code) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.message = message;
            this.code = code;
        }
        
        // Getters...
    }
    
    public static class GlobalError {
        private final String message;
        private final String code;
        
        public GlobalError(String message, String code) {
            this.message = message;
            this.code = code;
        }
        
        // Getters...
    }
}

Custom Exception Types

Business Exception Hierarchy

public abstract class BaseException extends RuntimeException {
    private final String errorCode;
    
    public BaseException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BaseException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}

public class BusinessException extends BaseException {
    public BusinessException(String errorCode, String message) {
        super(errorCode, message);
    }
    
    public BusinessException(String errorCode, String message, Throwable cause) {
        super(errorCode, message, cause);
    }
}

public class ValidationException extends BaseException {
    private final List<ValidationError> validationErrors;
    
    public ValidationException(String message, List<ValidationError> validationErrors) {
        super("VALIDATION_ERROR", message);
        this.validationErrors = validationErrors;
    }
    
    public ValidationException(String message, Throwable cause) {
        super("VALIDATION_ERROR", message, cause);
        this.validationErrors = Collections.emptyList();
    }
    
    public List<ValidationError> getValidationErrors() {
        return validationErrors;
    }
    
    public static class ValidationError {
        private final String field;
        private final String message;
        
        public ValidationError(String field, String message) {
            this.field = field;
            this.message = message;
        }
        
        // Getters...
    }
}

public class ResourceNotFoundException extends BaseException {
    public ResourceNotFoundException(String message) {
        super("RESOURCE_NOT_FOUND", message);
    }
    
    public ResourceNotFoundException(String message, Throwable cause) {
        super("RESOURCE_NOT_FOUND", message, cause);
    }
}

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

public class AuthenticationException extends BaseException {
    public AuthenticationException(String message) {
        super("AUTHENTICATION_FAILED", message);
    }
    
    public AuthenticationException(String message, Throwable cause) {
        super("AUTHENTICATION_FAILED", message, cause);
    }
}

public class AuthorizationException extends BaseException {
    public AuthorizationException(String message) {
        super("AUTHORIZATION_FAILED", message);
    }
    
    public AuthorizationException(String message, Throwable cause) {
        super("AUTHORIZATION_FAILED", message, cause);
    }
}

Error Handling Configuration

Error Properties Configuration

@Configuration
@EnableConfigurationProperties(ErrorProperties.class)
public class ErrorHandlingConfiguration {
    
    @Bean
    @ConditionalOnMissingBean(ErrorAttributes.class)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
    }
    
    @Bean
    @ConditionalOnMissingBean(ErrorWebExceptionHandler.class)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
                                                           WebProperties webProperties,
                                                           ObjectProvider<ViewResolver> viewResolvers,
                                                           ServerCodecConfigurer serverCodecConfigurer,
                                                           ApplicationContext applicationContext) {
        DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(
            errorAttributes, webProperties.getResources(), new ErrorProperties(), applicationContext);
        exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
        exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
    
    @Bean
    @ConditionalOnProperty(name = "app.error.detailed", havingValue = "true")
    public ErrorAttributes detailedErrorAttributes() {
        return new DetailedErrorAttributes();
    }
}

Custom Error Attributes

public class DetailedErrorAttributes extends DefaultErrorAttributes {
    
    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
        
        // Add custom attributes
        errorAttributes.put("requestId", getRequestId(request));
        errorAttributes.put("userAgent", getUserAgent(request));
        errorAttributes.put("remoteAddress", getRemoteAddress(request));
        
        // Add detailed stack trace in development
        if (isDevelopmentMode()) {
            Throwable error = getError(request);
            if (error != null) {
                errorAttributes.put("detailedTrace", getDetailedTrace(error));
            }
        }
        
        return errorAttributes;
    }
    
    private String getRequestId(ServerRequest request) {
        return request.headers().firstHeader("X-Request-ID");
    }
    
    private String getUserAgent(ServerRequest request) {
        return request.headers().firstHeader(HttpHeaders.USER_AGENT);
    }
    
    private String getRemoteAddress(ServerRequest request) {
        return request.remoteAddress()
            .map(address -> address.getAddress().getHostAddress())
            .orElse("unknown");
    }
    
    private boolean isDevelopmentMode() {
        return Arrays.asList(environment.getActiveProfiles()).contains("dev");
    }
    
    private String getDetailedTrace(Throwable error) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        error.printStackTrace(pw);
        return sw.toString();
    }
}

Content Negotiation in Error Handling

Multi-Format Error Responses

@Component
public class ContentNegotiatingErrorHandler extends AbstractErrorWebExceptionHandler {
    
    private final ObjectMapper objectMapper;
    
    public ContentNegotiatingErrorHandler(ErrorAttributes errorAttributes,
                                        WebProperties.Resources resources,
                                        ApplicationContext applicationContext,
                                        ObjectMapper objectMapper) {
        super(errorAttributes, resources, applicationContext);
        this.objectMapper = objectMapper;
    }
    
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }
    
    private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        Map<String, Object> errorAttributes = getErrorAttributes(request, ErrorAttributeOptions.defaults());
        List<MediaType> acceptableTypes = request.headers().accept();
        
        if (acceptableTypes.contains(MediaType.APPLICATION_JSON) || acceptableTypes.isEmpty()) {
            return renderJsonError(request, errorAttributes);
        } else if (acceptableTypes.contains(MediaType.APPLICATION_XML)) {
            return renderXmlError(request, errorAttributes);
        } else if (acceptableTypes.contains(MediaType.TEXT_HTML)) {
            return renderHtmlError(request, errorAttributes);
        } else {
            return renderPlainTextError(request, errorAttributes);
        }
    }
    
    private Mono<ServerResponse> renderJsonError(ServerRequest request, Map<String, Object> errorAttributes) {
        ErrorResponse errorResponse = createErrorResponse(request, errorAttributes);
        int status = (int) errorAttributes.getOrDefault("status", 500);
        
        return ServerResponse.status(status)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> renderXmlError(ServerRequest request, Map<String, Object> errorAttributes) {
        ErrorResponse errorResponse = createErrorResponse(request, errorAttributes);
        int status = (int) errorAttributes.getOrDefault("status", 500);
        
        return ServerResponse.status(status)
            .contentType(MediaType.APPLICATION_XML)
            .bodyValue(errorResponse);
    }
    
    private Mono<ServerResponse> renderHtmlError(ServerRequest request, Map<String, Object> errorAttributes) {
        int status = (int) errorAttributes.getOrDefault("status", 500);
        String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
        
        String html = String.format("""
            <!DOCTYPE html>
            <html>
            <head><title>Error %d</title></head>
            <body>
                <h1>Error %d</h1>
                <p>%s</p>
                <p>Path: %s</p>
                <p>Timestamp: %s</p>
            </body>
            </html>
            """, status, status, message, request.path(), Instant.now());
        
        return ServerResponse.status(status)
            .contentType(MediaType.TEXT_HTML)
            .bodyValue(html);
    }
    
    private Mono<ServerResponse> renderPlainTextError(ServerRequest request, Map<String, Object> errorAttributes) {
        int status = (int) errorAttributes.getOrDefault("status", 500);
        String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
        
        String text = String.format("Error %d: %s\nPath: %s\nTimestamp: %s", 
            status, message, request.path(), Instant.now());
        
        return ServerResponse.status(status)
            .contentType(MediaType.TEXT_PLAIN)
            .bodyValue(text);
    }
    
    private ErrorResponse createErrorResponse(ServerRequest request, Map<String, Object> errorAttributes) {
        int status = (int) errorAttributes.getOrDefault("status", 500);
        String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
        String error = (String) errorAttributes.get("error");
        
        return ErrorResponse.builder()
            .code(error != null ? error.toUpperCase().replace(" ", "_") : "INTERNAL_ERROR")
            .message(message)
            .path(request.path())
            .timestamp(Instant.now())
            .build();
    }
}

Testing Error Handlers

@WebFluxTest
class ErrorHandlingTest {
    
    @Autowired
    private WebTestClient webTestClient;
    
    @MockBean
    private UserService userService;
    
    @Test
    void shouldReturnNotFoundForMissingUser() {
        String userId = "nonexistent";
        when(userService.findById(userId)).thenReturn(Mono.empty());
        
        webTestClient.get()
            .uri("/api/users/{id}", userId)
            .exchange()
            .expectStatus().isNotFound()
            .expectHeader().contentType(MediaType.APPLICATION_JSON)
            .expectBody()
            .jsonPath("$.code").isEqualTo("USER_NOT_FOUND")
            .jsonPath("$.message").value(containsString("User not found"))
            .jsonPath("$.path").isEqualTo("/api/users/" + userId)
            .jsonPath("$.timestamp").exists();
    }
    
    @Test
    void shouldReturnValidationErrorForInvalidUser() {
        User invalidUser = new User("", null); // Invalid user
        
        webTestClient.post()
            .uri("/api/users")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(invalidUser)
            .exchange()
            .expectStatus().isBadRequest()
            .expectHeader().contentType(MediaType.APPLICATION_JSON)
            .expectBody()
            .jsonPath("$.code").isEqualTo("VALIDATION_ERROR")
            .jsonPath("$.details.fieldErrors").isArray()
            .jsonPath("$.details.fieldErrors[0].field").exists()
            .jsonPath("$.details.fieldErrors[0].message").exists();
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-webflux

docs

annotation-controllers.md

configuration.md

error-handling.md

functional-routing.md

index.md

server-configuration.md

testing.md

webclient.md

tile.json