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
72 lines
4.7 KiB
Markdown
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.
|