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
52 lines
2.9 KiB
Markdown
52 lines
2.9 KiB
Markdown
# 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`.
|