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:
2026-09-11 17:12:22 +00:00
co-authored by Claude Opus 5
parent a065696478
commit 926250e1a9
63 changed files with 2414 additions and 0 deletions
@@ -0,0 +1,67 @@
# Profile: boot-flag (spring-boot 4.1.1, spring-framework 7.0.9)
## domain exception (OrderNotFoundException)
$ curl -X GET /orders/999
HTTP 500 Content-Type: application/json
{"timestamp":"2026-09-11T17:03:12.637Z","status":500,"error":"Internal Server Error","path":"/orders/999"}
## type mismatch (/orders/abc)
$ curl -X GET /orders/abc
HTTP 400 Content-Type: application/problem+json
{"detail":"Failed to convert 'id' with value: 'abc'","instance":"/orders/abc","status":400,"title":"Bad Request"}
## invalid body (@Valid)
$ curl -X POST /orders -d '{"sku":"","quantity":0,"customerEmail":"nope"}'
HTTP 400 Content-Type: application/problem+json
{"detail":"Invalid request content.","instance":"/orders","status":400,"title":"Bad Request"}
## invalid @RequestParam (@Max)
$ curl -X GET /orders?limit=500
HTTP 400 Content-Type: application/problem+json
{"detail":"Validation failure","instance":"/orders","status":400,"title":"Bad Request"}
## ErrorResponseException (OutOfStock)
$ curl -X POST /orders -d '{"sku":"SKU-2","quantity":3,"customerEmail":"[email protected]"}'
HTTP 409 Content-Type: application/problem+json
{"detail":"Only 0 unit(s) of SKU-2 are available.","instance":"/orders","status":409,"title":"Out of stock","type":"https://ankurm.com/problems/out-of-stock","sku":"SKU-2","available":0}
## ResponseStatusException
$ curl -X GET /orders/legacy/7
HTTP 410 Content-Type: application/problem+json
{"detail":"Legacy order ids were retired in 2024","instance":"/orders/legacy/7","status":410,"title":"Gone"}
## unknown path
$ curl -X GET /no-such-thing
HTTP 404 Content-Type: application/problem+json
{"detail":"No static resource no-such-thing.","instance":"/no-such-thing","status":404,"title":"Not Found"}
## wrong HTTP method
$ curl -X DELETE /orders/1
HTTP 405 Content-Type: application/problem+json
{"detail":"Method 'DELETE' is not supported.","instance":"/orders/1","status":405,"title":"Method Not Allowed"}
## malformed JSON
$ curl -X POST /orders -d '{"sku":'
HTTP 400 Content-Type: application/problem+json
{"detail":"Failed to read request","instance":"/orders","status":400,"title":"Bad Request"}
## unexpected exception
$ curl -X GET /orders/boom
HTTP 500 Content-Type: application/json
{"timestamp":"2026-09-11T17:03:13.097Z","status":500,"error":"Internal Server Error","path":"/orders/boom"}
## exception in a servlet filter
$ curl -X GET /orders/1 [X-Tenant: BAD!]
HTTP 500 Content-Type: application/json
{"timestamp":"2026-09-11T17:03:13.125Z","status":500,"error":"Internal Server Error","path":"/orders/1"}
## 401 no credentials
$ curl -X GET /admin/orders
HTTP 401 Content-Type: application/json
{"timestamp":"2026-09-11T17:03:13.161Z","status":401,"error":"Unauthorized","path":"/admin/orders"}
## 403 wrong role
$ curl -X GET /admin/orders [AUTH:user:user]
HTTP 403 Content-Type: application/json
{"timestamp":"2026-09-11T17:03:13.399Z","status":403,"error":"Forbidden","path":"/admin/orders"}