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
2.9 KiB
6. Errors that never reach an advice
← 5. Content negotiation · Index · Next: 7. Silent 500s →
@ControllerAdvice runs inside the DispatcherServlet. Three kinds of error happen before or
around it.
Exceptions thrown by servlet filters
TenantHeaderFilter 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
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):
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
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 serialises one
ProblemDetail with one extension member three ways (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.