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,123 @@
# Profile: advice
## RestClient GET /orders/999 -> ex.getResponseBodyAs(ProblemDetail.class)
{
"exception": "org.springframework.web.client.HttpClientErrorException$NotFound",
"status": 404,
"contentType": "application/problem+json",
"problemDetail": {
"type": "https://ankurm.com/problems/order-not-found",
"title": "Order not found",
"status": 404,
"detail": "Order 999 does not exist",
"instance": "/orders/999",
"properties": {
"orderId": 999
}
}
}
## RestClient GET /orders?limit=500 -> ex.getResponseBodyAs(ProblemDetail.class)
{
"exception": "org.springframework.web.client.HttpClientErrorException$BadRequest",
"status": 400,
"contentType": "application/problem+json",
"problemDetail": {
"type": "null",
"title": "Bad Request",
"status": 400,
"detail": "Validation failure",
"instance": "/orders",
"properties": {
"errors": [
{
"parameter": "limit",
"detail": "must be less than or equal to 100"
}
]
}
}
}
## RestClient GET /orders/boom -> ex.getResponseBodyAs(ProblemDetail.class)
{
"exception": "org.springframework.web.client.HttpServerErrorException$InternalServerError",
"status": 500,
"contentType": "application/problem+json",
"problemDetail": {
"type": "null",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Quote errorId when reporting it.",
"instance": "/orders/boom",
"properties": {
"errorId": "d09000a5-d6bf-438d-a1a0-527d7cd9d6b7"
}
}
}
# Profile: defaults
## RestClient GET /orders/999 -> ex.getResponseBodyAs(ProblemDetail.class)
{
"exception": "org.springframework.web.client.HttpServerErrorException$InternalServerError",
"status": 500,
"contentType": "application/json",
"problemDetail": {
"type": "null",
"title": "Internal Server Error",
"status": 500,
"detail": null,
"instance": "null",
"properties": {
"timestamp": "2026-09-11T17:05:01.408Z",
"error": "Internal Server Error",
"path": "/orders/999"
}
}
}
## RestClient GET /orders?limit=500 -> ex.getResponseBodyAs(ProblemDetail.class)
{
"exception": "org.springframework.web.client.HttpClientErrorException$BadRequest",
"status": 400,
"contentType": "application/json",
"problemDetail": {
"type": "null",
"title": "Bad Request",
"status": 400,
"detail": null,
"instance": "null",
"properties": {
"timestamp": "2026-09-11T17:05:01.620Z",
"error": "Bad Request",
"path": "/orders"
}
}
}
## RestClient GET /orders/boom -> ex.getResponseBodyAs(ProblemDetail.class)
{
"exception": "org.springframework.web.client.HttpServerErrorException$InternalServerError",
"status": 500,
"contentType": "application/json",
"problemDetail": {
"type": "null",
"title": "Internal Server Error",
"status": 500,
"detail": null,
"instance": "null",
"properties": {
"timestamp": "2026-09-11T17:05:01.660Z",
"error": "Internal Server Error",
"path": "/orders/boom"
}
}
}
# ProblemDetail with one extension member, serialised three ways
{
"Spring Boot's JsonMapper bean": "{\"detail\":\"demo\",\"status\":409,\"title\":\"Conflict\",\"sku\":\"SKU-2\"}",
"JsonMapper.builder().build()": "{\"detail\":\"demo\",\"instance\":null,\"properties\":{\"sku\":\"SKU-2\"},\"status\":409,\"title\":\"Conflict\",\"type\":null}",
"new JsonMapper()": "{\"detail\":\"demo\",\"instance\":null,\"properties\":{\"sku\":\"SKU-2\"},\"status\":409,\"title\":\"Conflict\",\"type\":null}"
}