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
5.2 KiB
5.2 KiB
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
export JAVA_HOME=/path/to/jdk-25
mvn -DskipTests package
./scripts/run.sh advice,errors # the recommended setup
curl -s localhost:8080/orders/999
./scripts/run-all.sh # regenerate every transcript in docs/output/
mvn test # 16 contract tests
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:blankchange in 7.0 - Content negotiation: why
Accept: application/xmlgets 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
| File | Produced by |
|---|---|
matrix-summary.txt and matrix-<profile>.txt |
scripts/demo-matrix.sh |
content-negotiation.txt |
scripts/demo-negotiation.sh |
i18n.txt |
scripts/demo-i18n.sh |
silent-500.txt |
scripts/demo-silent-500.sh |
ambiguous-handler.txt |
scripts/demo-ambiguous.sh |
client-decoding.txt |
scripts/demo-client.sh |
advice-order.txt |
scripts/demo-advice-order.sh |
type-default.txt |
scripts/demo-type-default.sh |
errors-only.txt |
scripts/demo-errors-only.sh |
Findings worth the trip
spring.mvc.problemdetails.enabled=truecovers Spring MVC's own exceptions only. Your domain exceptions, unexpected 500s, filter exceptions and Security's 401/403 keep Boot's/errorJSON - two error shapes in one API.- Spring Framework 7 stopped defaulting
typetoabout:blank. 6.2.19 returns the URI; 7.0.9 returnsnulland the member disappears from the JSON. Accept: application/xmlgets a JSON error even with Jackson XML present, becauseapplication/xmlis not compatible withapplication/problem+xml.- A
new JsonMapper()rendersProblemDetailwrongly -"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.