or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

annotation-controllers.mdconfiguration.mderror-handling.mdfunctional-routing.mdindex.mdserver-configuration.mdtesting.mdwebclient.md

error-handling.mddocs/

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

```