0
# Error Handling
1
2
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.
3
4
## Core Error Handling Interfaces
5
6
### ErrorWebExceptionHandler
7
8
```java { .api }
9
public interface ErrorWebExceptionHandler extends WebExceptionHandler {
10
Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
11
}
12
13
@FunctionalInterface
14
public interface WebExceptionHandler {
15
Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
16
}
17
```
18
19
### AbstractErrorWebExceptionHandler
20
21
```java { .api }
22
public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExceptionHandler, ApplicationContextAware {
23
24
public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes,
25
WebProperties.Resources resources,
26
ApplicationContext applicationContext);
27
28
protected abstract RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes);
29
30
protected void setMessageWriters(List<HttpMessageWriter<?>> messageWriters);
31
protected void setMessageReaders(List<HttpMessageReader<?>> messageReaders);
32
protected void setViewResolvers(List<ViewResolver> viewResolvers);
33
34
protected ServerRequest createServerRequest(ServerWebExchange exchange, Throwable ex);
35
protected void logError(ServerRequest request, ServerResponse response, Throwable throwable);
36
37
@Override
38
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex);
39
}
40
```
41
42
### DefaultErrorWebExceptionHandler
43
44
```java { .api }
45
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
46
47
public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes,
48
WebProperties.Resources resources,
49
ErrorProperties errorProperties,
50
ApplicationContext applicationContext);
51
52
@Override
53
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes);
54
55
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request);
56
protected Mono<ServerResponse> renderErrorView(ServerRequest request);
57
protected int getHttpStatus(Map<String, Object> errorAttributes);
58
protected Mono<String> renderDefaultErrorView(ServerRequest request);
59
}
60
```
61
62
## Error Attributes
63
64
### ErrorAttributes Interface
65
66
```java { .api }
67
public interface ErrorAttributes {
68
69
Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options);
70
Throwable getError(ServerRequest request);
71
void storeErrorInformation(Throwable error, ServerWebExchange exchange);
72
}
73
74
public class ErrorAttributeOptions {
75
76
public static ErrorAttributeOptions defaults();
77
public static ErrorAttributeOptions of(Include... includes);
78
79
public ErrorAttributeOptions including(Include... includes);
80
public ErrorAttributeOptions excluding(Include... excludes);
81
82
public enum Include {
83
EXCEPTION, STACK_TRACE, MESSAGE, BINDING_ERRORS
84
}
85
}
86
```
87
88
### DefaultErrorAttributes
89
90
```java { .api }
91
public class DefaultErrorAttributes implements ErrorAttributes {
92
93
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
94
95
@Override
96
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options);
97
98
@Override
99
public Throwable getError(ServerRequest request);
100
101
@Override
102
public void storeErrorInformation(Throwable error, ServerWebExchange exchange);
103
104
protected String getTrace(Throwable ex);
105
protected void addStackTrace(Map<String, Object> errorAttributes, Throwable ex);
106
protected void addErrorMessage(Map<String, Object> errorAttributes, Throwable ex);
107
protected void addBindingResults(Map<String, Object> errorAttributes, ServerRequest request);
108
}
109
```
110
111
## Custom Error Handlers
112
113
### Global Error Handler
114
115
```java
116
@Component
117
@Order(-2) // Higher precedence than DefaultErrorWebExceptionHandler
118
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
119
120
public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes,
121
WebProperties.Resources resources,
122
ApplicationContext applicationContext) {
123
super(errorAttributes, resources, applicationContext);
124
}
125
126
@Override
127
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
128
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
129
}
130
131
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
132
Map<String, Object> errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());
133
Throwable error = getError(request);
134
135
return switch (error) {
136
case ValidationException ve -> handleValidationError(request, ve);
137
case ResourceNotFoundException rnfe -> handleResourceNotFound(request, rnfe);
138
case AuthenticationException ae -> handleAuthenticationError(request, ae);
139
case AuthorizationException aze -> handleAuthorizationError(request, aze);
140
case BusinessException be -> handleBusinessError(request, be);
141
default -> handleGenericError(request, errorPropertiesMap);
142
};
143
}
144
145
private Mono<ServerResponse> handleValidationError(ServerRequest request, ValidationException ex) {
146
ErrorResponse errorResponse = ErrorResponse.builder()
147
.code("VALIDATION_ERROR")
148
.message(ex.getMessage())
149
.details(ex.getValidationErrors())
150
.timestamp(Instant.now())
151
.path(request.path())
152
.build();
153
154
return ServerResponse.badRequest()
155
.contentType(MediaType.APPLICATION_JSON)
156
.bodyValue(errorResponse);
157
}
158
159
private Mono<ServerResponse> handleResourceNotFound(ServerRequest request, ResourceNotFoundException ex) {
160
ErrorResponse errorResponse = ErrorResponse.builder()
161
.code("RESOURCE_NOT_FOUND")
162
.message(ex.getMessage())
163
.timestamp(Instant.now())
164
.path(request.path())
165
.build();
166
167
return ServerResponse.notFound()
168
.contentType(MediaType.APPLICATION_JSON)
169
.bodyValue(errorResponse);
170
}
171
172
private Mono<ServerResponse> handleAuthenticationError(ServerRequest request, AuthenticationException ex) {
173
ErrorResponse errorResponse = ErrorResponse.builder()
174
.code("AUTHENTICATION_FAILED")
175
.message("Authentication required")
176
.timestamp(Instant.now())
177
.path(request.path())
178
.build();
179
180
return ServerResponse.status(HttpStatus.UNAUTHORIZED)
181
.contentType(MediaType.APPLICATION_JSON)
182
.bodyValue(errorResponse);
183
}
184
185
private Mono<ServerResponse> handleAuthorizationError(ServerRequest request, AuthorizationException ex) {
186
ErrorResponse errorResponse = ErrorResponse.builder()
187
.code("AUTHORIZATION_FAILED")
188
.message("Insufficient permissions")
189
.timestamp(Instant.now())
190
.path(request.path())
191
.build();
192
193
return ServerResponse.status(HttpStatus.FORBIDDEN)
194
.contentType(MediaType.APPLICATION_JSON)
195
.bodyValue(errorResponse);
196
}
197
198
private Mono<ServerResponse> handleBusinessError(ServerRequest request, BusinessException ex) {
199
ErrorResponse errorResponse = ErrorResponse.builder()
200
.code(ex.getErrorCode())
201
.message(ex.getMessage())
202
.timestamp(Instant.now())
203
.path(request.path())
204
.build();
205
206
return ServerResponse.status(HttpStatus.UNPROCESSABLE_ENTITY)
207
.contentType(MediaType.APPLICATION_JSON)
208
.bodyValue(errorResponse);
209
}
210
211
private Mono<ServerResponse> handleGenericError(ServerRequest request, Map<String, Object> errorAttributes) {
212
int status = (int) errorAttributes.getOrDefault("status", 500);
213
String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
214
215
ErrorResponse errorResponse = ErrorResponse.builder()
216
.code("INTERNAL_ERROR")
217
.message(message)
218
.timestamp(Instant.now())
219
.path(request.path())
220
.build();
221
222
return ServerResponse.status(status)
223
.contentType(MediaType.APPLICATION_JSON)
224
.bodyValue(errorResponse);
225
}
226
}
227
```
228
229
### Controller-Level Error Handling
230
231
```java
232
@RestController
233
@RequestMapping("/api/users")
234
public class UserController {
235
236
private final UserService userService;
237
238
public UserController(UserService userService) {
239
this.userService = userService;
240
}
241
242
@GetMapping("/{id}")
243
public Mono<User> getUser(@PathVariable String id) {
244
return userService.findById(id)
245
.switchIfEmpty(Mono.error(new UserNotFoundException("User not found: " + id)))
246
.onErrorMap(DataAccessException.class, ex ->
247
new ServiceException("Database error while fetching user", ex));
248
}
249
250
@PostMapping
251
public Mono<User> createUser(@Valid @RequestBody Mono<User> userMono) {
252
return userMono
253
.flatMap(userService::save)
254
.onErrorMap(ConstraintViolationException.class, ex ->
255
new ValidationException("User validation failed", ex))
256
.onErrorMap(DataIntegrityViolationException.class, ex ->
257
new BusinessException("USER_ALREADY_EXISTS", "User already exists"));
258
}
259
260
@PutMapping("/{id}")
261
public Mono<User> updateUser(@PathVariable String id, @Valid @RequestBody Mono<User> userMono) {
262
return userMono
263
.flatMap(user -> userService.update(id, user))
264
.switchIfEmpty(Mono.error(new UserNotFoundException("User not found: " + id)))
265
.onErrorMap(OptimisticLockingFailureException.class, ex ->
266
new BusinessException("CONCURRENT_MODIFICATION", "User was modified by another process"));
267
}
268
269
// Controller-specific exception handlers
270
@ExceptionHandler(UserNotFoundException.class)
271
public Mono<ResponseEntity<ErrorResponse>> handleUserNotFound(UserNotFoundException ex) {
272
ErrorResponse error = ErrorResponse.builder()
273
.code("USER_NOT_FOUND")
274
.message(ex.getMessage())
275
.timestamp(Instant.now())
276
.build();
277
278
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(error));
279
}
280
281
@ExceptionHandler(ValidationException.class)
282
public Mono<ResponseEntity<ErrorResponse>> handleValidation(ValidationException ex) {
283
ErrorResponse error = ErrorResponse.builder()
284
.code("VALIDATION_ERROR")
285
.message(ex.getMessage())
286
.details(ex.getValidationErrors())
287
.timestamp(Instant.now())
288
.build();
289
290
return Mono.just(ResponseEntity.badRequest().body(error));
291
}
292
}
293
```
294
295
### Functional Error Handling
296
297
```java
298
@Configuration
299
public class ErrorHandlingRouterConfiguration {
300
301
@Bean
302
public RouterFunction<ServerResponse> userRoutes(UserHandler userHandler) {
303
return RouterFunctions.route()
304
.GET("/api/users/{id}", userHandler::getUser)
305
.POST("/api/users", userHandler::createUser)
306
.onError(UserNotFoundException.class, this::handleUserNotFound)
307
.onError(ValidationException.class, this::handleValidationError)
308
.onError(Exception.class, this::handleGenericError)
309
.build();
310
}
311
312
private Mono<ServerResponse> handleUserNotFound(Throwable ex, ServerRequest request) {
313
ErrorResponse error = ErrorResponse.builder()
314
.code("USER_NOT_FOUND")
315
.message(ex.getMessage())
316
.path(request.path())
317
.timestamp(Instant.now())
318
.build();
319
320
return ServerResponse.notFound()
321
.contentType(MediaType.APPLICATION_JSON)
322
.bodyValue(error);
323
}
324
325
private Mono<ServerResponse> handleValidationError(Throwable ex, ServerRequest request) {
326
ValidationException validationEx = (ValidationException) ex;
327
ErrorResponse error = ErrorResponse.builder()
328
.code("VALIDATION_ERROR")
329
.message(validationEx.getMessage())
330
.details(validationEx.getValidationErrors())
331
.path(request.path())
332
.timestamp(Instant.now())
333
.build();
334
335
return ServerResponse.badRequest()
336
.contentType(MediaType.APPLICATION_JSON)
337
.bodyValue(error);
338
}
339
340
private Mono<ServerResponse> handleGenericError(Throwable ex, ServerRequest request) {
341
log.error("Unexpected error in request: {} {}", request.method(), request.path(), ex);
342
343
ErrorResponse error = ErrorResponse.builder()
344
.code("INTERNAL_ERROR")
345
.message("An unexpected error occurred")
346
.path(request.path())
347
.timestamp(Instant.now())
348
.build();
349
350
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
351
.contentType(MediaType.APPLICATION_JSON)
352
.bodyValue(error);
353
}
354
}
355
```
356
357
## Error Response Models
358
359
### Standard Error Response
360
361
```java
362
public class ErrorResponse {
363
private final String code;
364
private final String message;
365
private final String path;
366
private final Instant timestamp;
367
private final Object details;
368
369
public static Builder builder() {
370
return new Builder();
371
}
372
373
public static class Builder {
374
private String code;
375
private String message;
376
private String path;
377
private Instant timestamp;
378
private Object details;
379
380
public Builder code(String code) {
381
this.code = code;
382
return this;
383
}
384
385
public Builder message(String message) {
386
this.message = message;
387
return this;
388
}
389
390
public Builder path(String path) {
391
this.path = path;
392
return this;
393
}
394
395
public Builder timestamp(Instant timestamp) {
396
this.timestamp = timestamp;
397
return this;
398
}
399
400
public Builder details(Object details) {
401
this.details = details;
402
return this;
403
}
404
405
public ErrorResponse build() {
406
return new ErrorResponse(code, message, path, timestamp, details);
407
}
408
}
409
410
// Getters...
411
}
412
```
413
414
### Validation Error Details
415
416
```java
417
public class ValidationErrorDetails {
418
private final List<FieldError> fieldErrors;
419
private final List<GlobalError> globalErrors;
420
421
public ValidationErrorDetails(List<FieldError> fieldErrors, List<GlobalError> globalErrors) {
422
this.fieldErrors = fieldErrors != null ? fieldErrors : Collections.emptyList();
423
this.globalErrors = globalErrors != null ? globalErrors : Collections.emptyList();
424
}
425
426
public static class FieldError {
427
private final String field;
428
private final Object rejectedValue;
429
private final String message;
430
private final String code;
431
432
public FieldError(String field, Object rejectedValue, String message, String code) {
433
this.field = field;
434
this.rejectedValue = rejectedValue;
435
this.message = message;
436
this.code = code;
437
}
438
439
// Getters...
440
}
441
442
public static class GlobalError {
443
private final String message;
444
private final String code;
445
446
public GlobalError(String message, String code) {
447
this.message = message;
448
this.code = code;
449
}
450
451
// Getters...
452
}
453
}
454
```
455
456
## Custom Exception Types
457
458
### Business Exception Hierarchy
459
460
```java
461
public abstract class BaseException extends RuntimeException {
462
private final String errorCode;
463
464
public BaseException(String errorCode, String message) {
465
super(message);
466
this.errorCode = errorCode;
467
}
468
469
public BaseException(String errorCode, String message, Throwable cause) {
470
super(message, cause);
471
this.errorCode = errorCode;
472
}
473
474
public String getErrorCode() {
475
return errorCode;
476
}
477
}
478
479
public class BusinessException extends BaseException {
480
public BusinessException(String errorCode, String message) {
481
super(errorCode, message);
482
}
483
484
public BusinessException(String errorCode, String message, Throwable cause) {
485
super(errorCode, message, cause);
486
}
487
}
488
489
public class ValidationException extends BaseException {
490
private final List<ValidationError> validationErrors;
491
492
public ValidationException(String message, List<ValidationError> validationErrors) {
493
super("VALIDATION_ERROR", message);
494
this.validationErrors = validationErrors;
495
}
496
497
public ValidationException(String message, Throwable cause) {
498
super("VALIDATION_ERROR", message, cause);
499
this.validationErrors = Collections.emptyList();
500
}
501
502
public List<ValidationError> getValidationErrors() {
503
return validationErrors;
504
}
505
506
public static class ValidationError {
507
private final String field;
508
private final String message;
509
510
public ValidationError(String field, String message) {
511
this.field = field;
512
this.message = message;
513
}
514
515
// Getters...
516
}
517
}
518
519
public class ResourceNotFoundException extends BaseException {
520
public ResourceNotFoundException(String message) {
521
super("RESOURCE_NOT_FOUND", message);
522
}
523
524
public ResourceNotFoundException(String message, Throwable cause) {
525
super("RESOURCE_NOT_FOUND", message, cause);
526
}
527
}
528
529
public class UserNotFoundException extends ResourceNotFoundException {
530
public UserNotFoundException(String message) {
531
super(message);
532
}
533
}
534
535
public class AuthenticationException extends BaseException {
536
public AuthenticationException(String message) {
537
super("AUTHENTICATION_FAILED", message);
538
}
539
540
public AuthenticationException(String message, Throwable cause) {
541
super("AUTHENTICATION_FAILED", message, cause);
542
}
543
}
544
545
public class AuthorizationException extends BaseException {
546
public AuthorizationException(String message) {
547
super("AUTHORIZATION_FAILED", message);
548
}
549
550
public AuthorizationException(String message, Throwable cause) {
551
super("AUTHORIZATION_FAILED", message, cause);
552
}
553
}
554
```
555
556
## Error Handling Configuration
557
558
### Error Properties Configuration
559
560
```java
561
@Configuration
562
@EnableConfigurationProperties(ErrorProperties.class)
563
public class ErrorHandlingConfiguration {
564
565
@Bean
566
@ConditionalOnMissingBean(ErrorAttributes.class)
567
public DefaultErrorAttributes errorAttributes() {
568
return new DefaultErrorAttributes();
569
}
570
571
@Bean
572
@ConditionalOnMissingBean(ErrorWebExceptionHandler.class)
573
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
574
WebProperties webProperties,
575
ObjectProvider<ViewResolver> viewResolvers,
576
ServerCodecConfigurer serverCodecConfigurer,
577
ApplicationContext applicationContext) {
578
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(
579
errorAttributes, webProperties.getResources(), new ErrorProperties(), applicationContext);
580
exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
581
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
582
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
583
return exceptionHandler;
584
}
585
586
@Bean
587
@ConditionalOnProperty(name = "app.error.detailed", havingValue = "true")
588
public ErrorAttributes detailedErrorAttributes() {
589
return new DetailedErrorAttributes();
590
}
591
}
592
```
593
594
### Custom Error Attributes
595
596
```java
597
public class DetailedErrorAttributes extends DefaultErrorAttributes {
598
599
@Override
600
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
601
Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
602
603
// Add custom attributes
604
errorAttributes.put("requestId", getRequestId(request));
605
errorAttributes.put("userAgent", getUserAgent(request));
606
errorAttributes.put("remoteAddress", getRemoteAddress(request));
607
608
// Add detailed stack trace in development
609
if (isDevelopmentMode()) {
610
Throwable error = getError(request);
611
if (error != null) {
612
errorAttributes.put("detailedTrace", getDetailedTrace(error));
613
}
614
}
615
616
return errorAttributes;
617
}
618
619
private String getRequestId(ServerRequest request) {
620
return request.headers().firstHeader("X-Request-ID");
621
}
622
623
private String getUserAgent(ServerRequest request) {
624
return request.headers().firstHeader(HttpHeaders.USER_AGENT);
625
}
626
627
private String getRemoteAddress(ServerRequest request) {
628
return request.remoteAddress()
629
.map(address -> address.getAddress().getHostAddress())
630
.orElse("unknown");
631
}
632
633
private boolean isDevelopmentMode() {
634
return Arrays.asList(environment.getActiveProfiles()).contains("dev");
635
}
636
637
private String getDetailedTrace(Throwable error) {
638
StringWriter sw = new StringWriter();
639
PrintWriter pw = new PrintWriter(sw);
640
error.printStackTrace(pw);
641
return sw.toString();
642
}
643
}
644
```
645
646
## Content Negotiation in Error Handling
647
648
### Multi-Format Error Responses
649
650
```java
651
@Component
652
public class ContentNegotiatingErrorHandler extends AbstractErrorWebExceptionHandler {
653
654
private final ObjectMapper objectMapper;
655
656
public ContentNegotiatingErrorHandler(ErrorAttributes errorAttributes,
657
WebProperties.Resources resources,
658
ApplicationContext applicationContext,
659
ObjectMapper objectMapper) {
660
super(errorAttributes, resources, applicationContext);
661
this.objectMapper = objectMapper;
662
}
663
664
@Override
665
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
666
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
667
}
668
669
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
670
Map<String, Object> errorAttributes = getErrorAttributes(request, ErrorAttributeOptions.defaults());
671
List<MediaType> acceptableTypes = request.headers().accept();
672
673
if (acceptableTypes.contains(MediaType.APPLICATION_JSON) || acceptableTypes.isEmpty()) {
674
return renderJsonError(request, errorAttributes);
675
} else if (acceptableTypes.contains(MediaType.APPLICATION_XML)) {
676
return renderXmlError(request, errorAttributes);
677
} else if (acceptableTypes.contains(MediaType.TEXT_HTML)) {
678
return renderHtmlError(request, errorAttributes);
679
} else {
680
return renderPlainTextError(request, errorAttributes);
681
}
682
}
683
684
private Mono<ServerResponse> renderJsonError(ServerRequest request, Map<String, Object> errorAttributes) {
685
ErrorResponse errorResponse = createErrorResponse(request, errorAttributes);
686
int status = (int) errorAttributes.getOrDefault("status", 500);
687
688
return ServerResponse.status(status)
689
.contentType(MediaType.APPLICATION_JSON)
690
.bodyValue(errorResponse);
691
}
692
693
private Mono<ServerResponse> renderXmlError(ServerRequest request, Map<String, Object> errorAttributes) {
694
ErrorResponse errorResponse = createErrorResponse(request, errorAttributes);
695
int status = (int) errorAttributes.getOrDefault("status", 500);
696
697
return ServerResponse.status(status)
698
.contentType(MediaType.APPLICATION_XML)
699
.bodyValue(errorResponse);
700
}
701
702
private Mono<ServerResponse> renderHtmlError(ServerRequest request, Map<String, Object> errorAttributes) {
703
int status = (int) errorAttributes.getOrDefault("status", 500);
704
String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
705
706
String html = String.format("""
707
<!DOCTYPE html>
708
<html>
709
<head><title>Error %d</title></head>
710
<body>
711
<h1>Error %d</h1>
712
<p>%s</p>
713
<p>Path: %s</p>
714
<p>Timestamp: %s</p>
715
</body>
716
</html>
717
""", status, status, message, request.path(), Instant.now());
718
719
return ServerResponse.status(status)
720
.contentType(MediaType.TEXT_HTML)
721
.bodyValue(html);
722
}
723
724
private Mono<ServerResponse> renderPlainTextError(ServerRequest request, Map<String, Object> errorAttributes) {
725
int status = (int) errorAttributes.getOrDefault("status", 500);
726
String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
727
728
String text = String.format("Error %d: %s\nPath: %s\nTimestamp: %s",
729
status, message, request.path(), Instant.now());
730
731
return ServerResponse.status(status)
732
.contentType(MediaType.TEXT_PLAIN)
733
.bodyValue(text);
734
}
735
736
private ErrorResponse createErrorResponse(ServerRequest request, Map<String, Object> errorAttributes) {
737
int status = (int) errorAttributes.getOrDefault("status", 500);
738
String message = (String) errorAttributes.getOrDefault("message", "Internal Server Error");
739
String error = (String) errorAttributes.get("error");
740
741
return ErrorResponse.builder()
742
.code(error != null ? error.toUpperCase().replace(" ", "_") : "INTERNAL_ERROR")
743
.message(message)
744
.path(request.path())
745
.timestamp(Instant.now())
746
.build();
747
}
748
}
749
```
750
751
## Testing Error Handlers
752
753
```java
754
@WebFluxTest
755
class ErrorHandlingTest {
756
757
@Autowired
758
private WebTestClient webTestClient;
759
760
@MockBean
761
private UserService userService;
762
763
@Test
764
void shouldReturnNotFoundForMissingUser() {
765
String userId = "nonexistent";
766
when(userService.findById(userId)).thenReturn(Mono.empty());
767
768
webTestClient.get()
769
.uri("/api/users/{id}", userId)
770
.exchange()
771
.expectStatus().isNotFound()
772
.expectHeader().contentType(MediaType.APPLICATION_JSON)
773
.expectBody()
774
.jsonPath("$.code").isEqualTo("USER_NOT_FOUND")
775
.jsonPath("$.message").value(containsString("User not found"))
776
.jsonPath("$.path").isEqualTo("/api/users/" + userId)
777
.jsonPath("$.timestamp").exists();
778
}
779
780
@Test
781
void shouldReturnValidationErrorForInvalidUser() {
782
User invalidUser = new User("", null); // Invalid user
783
784
webTestClient.post()
785
.uri("/api/users")
786
.contentType(MediaType.APPLICATION_JSON)
787
.bodyValue(invalidUser)
788
.exchange()
789
.expectStatus().isBadRequest()
790
.expectHeader().contentType(MediaType.APPLICATION_JSON)
791
.expectBody()
792
.jsonPath("$.code").isEqualTo("VALIDATION_ERROR")
793
.jsonPath("$.details.fieldErrors").isArray()
794
.jsonPath("$.details.fieldErrors[0].field").exists()
795
.jsonPath("$.details.fieldErrors[0].message").exists();
796
}
797
}
798
```