Global exception handling with ProblemDetail (RFC 9457)
Companion project for Global Exception Handling with ProblemDetail (RFC 9457) in Spring Boot 4
on ankurm.com.
One small order API with thirteen ways to fail, run under five exception-handling setups. Every
table and transcript in the article came out of docs/output/, and
./scripts/run-all.sh regenerates all of it.
Versions
|
|
| Spring Boot |
4.1.1 |
| Spring Framework |
7.0.9 |
| Spring Security |
7.1.1 |
| Jackson |
3 (tools.jackson) |
| JDK |
Eclipse Temurin 25.0.4.1 (LTS) |
Quickstart
Profiles
| Profile |
What it activates |
| (none) |
Spring Boot defaults: no problem details anywhere |
boot-flag |
spring.mvc.problemdetails.enabled=true - Boot's ProblemDetailsExceptionHandler |
advice |
GlobalExceptionHandler + problem-detail security handlers |
errors |
ProblemDetailErrorController replacing BasicErrorController |
advice,errors |
the recommended combination - all thirteen failures become application/problem+json |
catchall-first |
a trap: a highest-precedence catch-all advice that turns every 4xx into 500 |
ambiguous |
a trap: a handler that stops the application starting |
Endpoints
| Endpoint |
Fails with |
GET /orders/{id} |
OrderNotFoundException (domain); TypeMismatchException for /orders/abc |
POST /orders |
MethodArgumentNotValidException; OutOfStockException (an ErrorResponseException) |
GET /orders?limit= |
HandlerMethodValidationException above 100 |
GET /orders/boom |
IllegalStateException whose message must not leak |
GET /orders/legacy/{id} |
ResponseStatusException |
any path with X-Tenant: BAD! |
exception thrown in a servlet filter |
GET /admin/orders |
401 / 403 from Spring Security |
GET /diag/advice |
diagnostic - every @ControllerAdvice in consultation order |
GET /diag/decode?path= |
diagnostic - what RestClient decodes from an error body |
GET /diag/mixin |
diagnostic - one ProblemDetail serialised by three mappers |
The /diag endpoints are for the article. Delete them before shipping anything.
Documentation
- The mental model: RFC 9457 and Spring's four types
- Choosing a mechanism - defaults, the Boot flag, an advice, the error controller
- Validation errors that say something
- Problem types, message codes and the
about:blank change in 7.0
- Content negotiation: why
Accept: application/xml gets JSON
- Errors that never reach an advice: filters, Security, and hand-built mappers
- Silent 500s: the catch-all that makes errors invisible
- The client side: decoding problems with
RestClient
Captured output
Findings worth the trip
spring.mvc.problemdetails.enabled=true covers Spring MVC's own exceptions only. Your
domain exceptions, unexpected 500s, filter exceptions and Security's 401/403 keep Boot's
/error JSON - two error shapes in one API.
- Spring Framework 7 stopped defaulting
type to about:blank. 6.2.19 returns the URI;
7.0.9 returns null and the member disappears from the JSON.
Accept: application/xml gets a JSON error even with Jackson XML present, because
application/xml is not compatible with application/problem+xml.
- A
new JsonMapper() renders ProblemDetail wrongly - "properties":{...} nested and
"type":null - so hand-written entry points must use the application's mapper.
- A catch-all advice that forgets to log makes every 500 invisible, and one given the highest
precedence also turns every 404, 405 and 400 into a 500.