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
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user