Files
spring-boot-demo/problem-details/README.md
T
asmhatreandClaude Opus 5 926250e1a9 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
2026-09-11 17:12:22 +00:00

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

  1. The mental model: RFC 9457 and Spring's four types
  2. Choosing a mechanism - defaults, the Boot flag, an advice, the error controller
  3. Validation errors that say something
  4. Problem types, message codes and the about:blank change in 7.0
  5. Content negotiation: why Accept: application/xml gets JSON
  6. Errors that never reach an advice: filters, Security, and hand-built mappers
  7. Silent 500s: the catch-all that makes errors invisible
  8. 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=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.