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
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# 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-<profile>.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.
|
||||
Reference in New Issue
Block a user