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,51 @@
|
||||
# 6. Errors that never reach an advice
|
||||
|
||||
[← 5. Content negotiation](05-content-negotiation.md) · [Index](../README.md) · Next: [7. Silent 500s →](07-silent-500s.md)
|
||||
|
||||
`@ControllerAdvice` runs inside the `DispatcherServlet`. Three kinds of error happen before or
|
||||
around it.
|
||||
|
||||
## Exceptions thrown by servlet filters
|
||||
|
||||
[`TenantHeaderFilter`](../src/main/java/com/ankurm/problems/web/TenantHeaderFilter.java) throws for
|
||||
a malformed header. No advice sees it; Tomcat forwards to `/error`, and even the `advice` profile
|
||||
answers with Boot's JSON. The fix is to own `/error`:
|
||||
[`ProblemDetailErrorController`](../src/main/java/com/ankurm/problems/advice/ProblemDetailErrorController.java)
|
||||
implements Boot's `ErrorController` (`org.springframework.boot.webmvc.error` in Boot 4), which makes
|
||||
`ErrorMvcAutoConfiguration` skip `BasicErrorController`. It builds the problem from
|
||||
`ErrorAttributes` and keeps the 5xx detail generic, because the message there is whatever a filter
|
||||
happened to throw.
|
||||
|
||||
## Spring Security's 401 and 403
|
||||
|
||||
Decided in the filter chain. By default the entry point calls `sendError(401)`, which also ends at
|
||||
`/error` - so the error controller alone turns them into problems ([`errors-only.txt`](output/errors-only.txt)):
|
||||
|
||||
```
|
||||
HTTP/1.1 401
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Unauthorized","instance":"/admin/orders","status":401,"title":"Unauthorized"}
|
||||
```
|
||||
This project goes one step
|
||||
further with [`ProblemDetailSecurityHandlers`](../src/main/java/com/ankurm/problems/security/ProblemDetailSecurityHandlers.java)
|
||||
so the 401 keeps a precise `type` and its `WWW-Authenticate` header.
|
||||
|
||||
Note it is registered for `httpBasic` as well as `exceptionHandling`: HTTP Basic has its own entry
|
||||
point and would otherwise bypass the one set on `exceptionHandling`.
|
||||
|
||||
## Writing a `ProblemDetail` yourself: use the application's mapper
|
||||
|
||||
[`/diag/mixin`](../src/main/java/com/ankurm/problems/diag/DiagController.java) serialises one
|
||||
`ProblemDetail` with one extension member three ways ([`client-decoding.txt`](output/client-decoding.txt)):
|
||||
|
||||
```
|
||||
"Spring Boot's JsonMapper bean": "{\"detail\":\"demo\",\"status\":409,\"title\":\"Conflict\",\"sku\":\"SKU-2\"}",
|
||||
"JsonMapper.builder().build()": "{\"detail\":\"demo\",\"instance\":null,\"properties\":{\"sku\":\"SKU-2\"},\"status\":409,\"title\":\"Conflict\",\"type\":null}",
|
||||
"new JsonMapper()": "{\"detail\":\"demo\",\"instance\":null,\"properties\":{\"sku\":\"SKU-2\"},\"status\":409,\"title\":\"Conflict\",\"type\":null}"
|
||||
```
|
||||
|
||||
`ProblemDetail` is a plain bean; the flattening of `properties` and the omission of empty members
|
||||
come from `ProblemDetailJacksonMixin`, which the Boot-configured mapper has and a mapper you
|
||||
construct does not. An entry point that does `new ObjectMapper().writeValue(...)` - the version in
|
||||
most tutorials - produces a body that is not a valid problem document. Inject the `JsonMapper`.
|
||||
Reference in New Issue
Block a user