Files
asmhatreandClaude Opus 5 926250e1a9 Add problem-details: global exception handling with RFC 9457
Companion code for "Global Exception Handling with ProblemDetail (RFC 9457)
in Spring Boot 4". Thirteen failures under five handling setups (Boot
defaults, the Boot flag, a ResponseEntityExceptionHandler advice, advice
plus an ErrorController, a catch-all ordered first), validation errors,
i18n, content negotiation, Security's 401/403, silent 500s and decoding
on the client. 16 tests pin the behaviour.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:12:22 +00:00

72 lines
4.7 KiB
Markdown

# 2. Choosing a mechanism
[← 1. Mental model](01-mental-model.md) · [Index](../README.md) · Next: [3. Validation errors →](03-validation-errors.md)
The same thirteen failures under five setups - [`matrix-summary.txt`](output/matrix-summary.txt),
produced by [`scripts/demo-matrix.sh`](../scripts/demo-matrix.sh):
```
failure | defaults | boot-flag | advice | advice,errors | catchall-first
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
domain exception (OrderNotFoundException) | 500 Boot /error JSON | 500 Boot /error JSON | 404 problem+json | 404 problem+json | 500 problem+json
type mismatch (/orders/abc) | 400 Boot /error JSON | 400 problem+json | 400 problem+json | 400 problem+json | 500 problem+json
exception in a servlet filter | 500 Boot /error JSON | 500 Boot /error JSON | 500 Boot /error JSON | 500 problem+json | 500 Boot /error JSON
401 no credentials | 401 Boot /error JSON | 401 Boot /error JSON | 401 problem+json | 401 problem+json | 401 Boot /error JSON
```
## `spring.mvc.problemdetails.enabled`
Defaults to `false` in Spring Boot 4.1.1 (`spring-configuration-metadata.json` in
`spring-boot-webmvc`). When `true`, `WebMvcAutoConfiguration$ProblemDetailsErrorHandlingConfiguration`
registers `ProblemDetailsExceptionHandler` - an empty subclass of `ResponseEntityExceptionHandler` -
with:
- `@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)` - it backs off as soon as you
declare your own subclass ([`advice-order.txt`](output/advice-order.txt), profiles
`boot-flag,advice`)
- `@Order(0)` - it is consulted before any advice with the default (lowest) precedence
It handles exactly what `ResponseEntityExceptionHandler` handles: Spring MVC's exceptions and any
`ErrorResponse`. **It does not handle your exceptions**, which is why the first and tenth rows stay
Boot's JSON. Turning the flag on and stopping there gives an API with two error shapes.
## Advice ordering, and the catch-all trap
`ExceptionHandlerExceptionResolver` walks the advices in order and uses the **first advice that has
any matching handler**, not the most specific handler across all advices. The
[`catchall-first`](../src/main/java/com/ankurm/problems/advice/CatchAllFirstHandler.java) profile
declares `@ExceptionHandler(Exception.class)` at `Ordered.HIGHEST_PRECEDENCE`; it matches
`NoResourceFoundException`, `HttpRequestMethodNotSupportedException` and every other framework
exception before Boot's handler at order 0 gets a look. Every 4xx in the matrix becomes a 500.
Keep the catch-all in the *same* class as your specific handlers (the most specific handler wins
within one class), or give it the lowest precedence.
## The handler that stops the application starting
Extending `ResponseEntityExceptionHandler` and adding
`@ExceptionHandler(MethodArgumentNotValidException.class)` - the obvious way to customise
validation errors - fails at startup ([`ambiguous-handler.txt`](output/ambiguous-handler.txt)):
```
Caused by: java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=org.springframework.web.bind.MethodArgumentNotValidException, mediaType=*/*}]:
{org.springframework.http.ProblemDetail com.ankurm.problems.advice.AmbiguousExceptionHandler.invalid(org.springframework.web.bind.MethodArgumentNotValidException),
public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}
```
The base class's `handleException` is `final` and already mapped. Override the protected
`handleMethodArgumentNotValid(...)` instead - [chapter 3](03-validation-errors.md).
## What the recommended setup is
`advice` + `errors`:
- [`GlobalExceptionHandler`](../src/main/java/com/ankurm/problems/advice/GlobalExceptionHandler.java)
extends `ResponseEntityExceptionHandler`, adds domain handlers and a logging catch-all
- [`ProblemDetailErrorController`](../src/main/java/com/ankurm/problems/advice/ProblemDetailErrorController.java)
covers the container error page - [chapter 6](06-outside-mvc.md)
- [`ProblemDetailSecurityHandlers`](../src/main/java/com/ankurm/problems/security/ProblemDetailSecurityHandlers.java)
covers 401/403 with a `WWW-Authenticate` header intact
[`AdviceContractTest`](../src/test/java/com/ankurm/problems/AdviceContractTest.java) pins it.