# Global exception handling with ProblemDetail (RFC 9457) Companion project for [**Global Exception Handling with ProblemDetail (RFC 9457) in Spring Boot 4**](https://ankurm.com/spring-boot-4-problemdetail-rfc-9457-global-exception-handling/) 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/`](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 ```bash 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`](src/main/java/com/ankurm/problems/advice/GlobalExceptionHandler.java) + [problem-detail security handlers](src/main/java/com/ankurm/problems/security/ProblemDetailSecurityHandlers.java) | | `errors` | [`ProblemDetailErrorController`](src/main/java/com/ankurm/problems/advice/ProblemDetailErrorController.java) 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](docs/01-mental-model.md) 2. [Choosing a mechanism - defaults, the Boot flag, an advice, the error controller](docs/02-choosing-a-mechanism.md) 3. [Validation errors that say something](docs/03-validation-errors.md) 4. [Problem types, message codes and the `about:blank` change in 7.0](docs/04-i18n-and-types.md) 5. [Content negotiation: why `Accept: application/xml` gets JSON](docs/05-content-negotiation.md) 6. [Errors that never reach an advice: filters, Security, and hand-built mappers](docs/06-outside-mvc.md) 7. [Silent 500s: the catch-all that makes errors invisible](docs/07-silent-500s.md) 8. [The client side: decoding problems with `RestClient`](docs/08-clients.md) ## Captured output | File | Produced by | |---|---| | [`matrix-summary.txt`](docs/output/matrix-summary.txt) and `matrix-.txt` | `scripts/demo-matrix.sh` | | [`content-negotiation.txt`](docs/output/content-negotiation.txt) | `scripts/demo-negotiation.sh` | | [`i18n.txt`](docs/output/i18n.txt) | `scripts/demo-i18n.sh` | | [`silent-500.txt`](docs/output/silent-500.txt) | `scripts/demo-silent-500.sh` | | [`ambiguous-handler.txt`](docs/output/ambiguous-handler.txt) | `scripts/demo-ambiguous.sh` | | [`client-decoding.txt`](docs/output/client-decoding.txt) | `scripts/demo-client.sh` | | [`advice-order.txt`](docs/output/advice-order.txt) | `scripts/demo-advice-order.sh` | | [`type-default.txt`](docs/output/type-default.txt) | `scripts/demo-type-default.sh` | | [`errors-only.txt`](docs/output/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.