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,15 @@
|
||||
## profiles: <none>
|
||||
|
||||
## profiles: boot-flag
|
||||
order 0 org.springframework.boot.webmvc.autoconfigure.ProblemDetailsExceptionHandler
|
||||
|
||||
## profiles: advice
|
||||
order 2147483647 com.ankurm.problems.advice.GlobalExceptionHandler
|
||||
|
||||
## profiles: boot-flag,advice
|
||||
order 2147483647 com.ankurm.problems.advice.GlobalExceptionHandler
|
||||
|
||||
## profiles: catchall-first
|
||||
order -2147483648 com.ankurm.problems.advice.CatchAllFirstHandler
|
||||
order 0 org.springframework.boot.webmvc.autoconfigure.ProblemDetailsExceptionHandler
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Profile: ambiguous (exit code 1)
|
||||
|
||||
Caused by: java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=org.springframework.web.bind.MethodArgumentNotValidException, mediaType=*/*}]:
|
||||
{org.springframework.http.ProblemDetail com.ankurm.problems.advice.AmbiguousExceptionHandler.invalid(org.springframework.web.bind.MethodArgumentNotValidException),
|
||||
public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Accept header vs error body (profile: advice, jackson-dataformat-xml on the classpath)
|
||||
|
||||
## A successful response, for comparison
|
||||
$ curl -H "Accept: application/xml" /orders/1
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/xml;charset=UTF-8
|
||||
<Order><id>1</id><sku>SKU-1</sku><quantity>2</quantity></Order>
|
||||
|
||||
## Accept: application/json
|
||||
$ curl -H "Accept: application/json" /orders/999
|
||||
HTTP/1.1 404
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":999}
|
||||
|
||||
## Accept: application/xml
|
||||
$ curl -H "Accept: application/xml" /orders/999
|
||||
HTTP/1.1 404
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":999}
|
||||
|
||||
## Accept: application/problem+xml
|
||||
$ curl -H "Accept: application/problem+xml" /orders/999
|
||||
HTTP/1.1 404
|
||||
Content-Type: application/problem+xml
|
||||
<problem xmlns="urn:ietf:rfc:7807"><detail>Order 999 does not exist</detail><instance>/orders/999</instance><status>404</status><title>Order not found</title><type>https://ankurm.com/problems/order-not-found</type><orderId>999</orderId></problem>
|
||||
|
||||
## Accept: text/html
|
||||
$ curl -H "Accept: text/html" /orders/999
|
||||
HTTP/1.1 404
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":999}
|
||||
|
||||
## Accept: image/png
|
||||
$ curl -H "Accept: image/png" /orders/999
|
||||
HTTP/1.1 404
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":999}
|
||||
|
||||
## Accept: */*
|
||||
$ curl -H "Accept: */*" /orders/999
|
||||
HTTP/1.1 404
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":999}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Profile: errors (ProblemDetailErrorController only - no @ControllerAdvice, no Security handlers)
|
||||
|
||||
$ curl /admin/orders
|
||||
HTTP/1.1 401
|
||||
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Unauthorized","instance":"/admin/orders","status":401,"title":"Unauthorized"}
|
||||
|
||||
$ curl -u user:user /admin/orders
|
||||
HTTP/1.1 403
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"Forbidden","instance":"/admin/orders","status":403,"title":"Forbidden"}
|
||||
|
||||
$ curl /orders/999
|
||||
HTTP/1.1 500
|
||||
Content-Type: application/problem+json
|
||||
{"detail":"An unexpected error occurred.","instance":"/orders/999","status":500,"title":"Internal Server Error"}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# OutOfStockException - its own ProblemDetail vs messages.properties vs messages_de.properties
|
||||
|
||||
## Exception's own text (what the constructor set):
|
||||
title="Insufficient stock" detail="Requested 3 of SKU-2 but only 0 available"
|
||||
|
||||
## Accept-Language: <none>
|
||||
$ curl -X POST /orders -d '{"sku":"SKU-2","quantity":3,"customerEmail":"[email protected]"}'
|
||||
{"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}
|
||||
|
||||
## Accept-Language: de
|
||||
$ curl -X POST /orders -d '{"sku":"SKU-2","quantity":3,"customerEmail":"[email protected]"}' -H 'Accept-Language: de'
|
||||
{"detail":"Von SKU-2 sind nur 0 Stück verfügbar.","instance":"/orders","status":409,"title":"Nicht vorrätig","type":"https://ankurm.com/problems/out-of-stock","sku":"SKU-2","available":0}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Profile: advice,errors (spring-boot 4.1.1, spring-framework 7.0.9)
|
||||
|
||||
## domain exception (OrderNotFoundException)
|
||||
$ curl -X GET /orders/999
|
||||
HTTP 404 Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":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":"The request body has 3 invalid field(s)","instance":"/orders","status":400,"title":"Bad Request","errors":[{"detail":"must not be blank","pointer":"#/sku"},{"detail":"must be a well-formed email address","pointer":"#/customerEmail"},{"detail":"must be greater than or equal to 1","pointer":"#/quantity"}]}
|
||||
|
||||
## 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","errors":[{"detail":"must be less than or equal to 100","parameter":"limit"}]}
|
||||
|
||||
## 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/problem+json
|
||||
{"detail":"An unexpected error occurred. Quote errorId when reporting it.","instance":"/orders/boom","status":500,"title":"Internal Server Error","errorId":"a3838ab0-afd2-4bbd-85b5-15e9a309c1f8"}
|
||||
|
||||
## exception in a servlet filter
|
||||
$ curl -X GET /orders/1 [X-Tenant: BAD!]
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"An unexpected error occurred.","instance":"/orders/1","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## 401 no credentials
|
||||
$ curl -X GET /admin/orders
|
||||
HTTP 401 Content-Type: application/problem+json
|
||||
{"detail":"Authentication is required to access this resource","instance":"/admin/orders","status":401,"title":"Unauthorized","type":"https://ankurm.com/problems/authentication-required","scheme":"Basic"}
|
||||
|
||||
## 403 wrong role
|
||||
$ curl -X GET /admin/orders [AUTH:user:user]
|
||||
HTTP 403 Content-Type: application/problem+json
|
||||
{"detail":"Your credentials do not grant access to this resource","instance":"/admin/orders","status":403,"title":"Forbidden","type":"https://ankurm.com/problems/access-denied"}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Profile: advice (spring-boot 4.1.1, spring-framework 7.0.9)
|
||||
|
||||
## domain exception (OrderNotFoundException)
|
||||
$ curl -X GET /orders/999
|
||||
HTTP 404 Content-Type: application/problem+json
|
||||
{"detail":"Order 999 does not exist","instance":"/orders/999","status":404,"title":"Order not found","type":"https://ankurm.com/problems/order-not-found","orderId":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":"The request body has 3 invalid field(s)","instance":"/orders","status":400,"title":"Bad Request","errors":[{"pointer":"#/quantity","detail":"must be greater than or equal to 1"},{"pointer":"#/customerEmail","detail":"must be a well-formed email address"},{"pointer":"#/sku","detail":"must not be blank"}]}
|
||||
|
||||
## 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","errors":[{"parameter":"limit","detail":"must be less than or equal to 100"}]}
|
||||
|
||||
## 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/problem+json
|
||||
{"detail":"An unexpected error occurred. Quote errorId when reporting it.","instance":"/orders/boom","status":500,"title":"Internal Server Error","errorId":"ac0a11a2-1ef5-4ef2-828f-21107e0b9008"}
|
||||
|
||||
## exception in a servlet filter
|
||||
$ curl -X GET /orders/1 [X-Tenant: BAD!]
|
||||
HTTP 500 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:21.460Z","status":500,"error":"Internal Server Error","path":"/orders/1"}
|
||||
|
||||
## 401 no credentials
|
||||
$ curl -X GET /admin/orders
|
||||
HTTP 401 Content-Type: application/problem+json
|
||||
{"detail":"Authentication is required to access this resource","instance":"/admin/orders","status":401,"title":"Unauthorized","type":"https://ankurm.com/problems/authentication-required","scheme":"Basic"}
|
||||
|
||||
## 403 wrong role
|
||||
$ curl -X GET /admin/orders [AUTH:user:user]
|
||||
HTTP 403 Content-Type: application/problem+json
|
||||
{"detail":"Your credentials do not grant access to this resource","instance":"/admin/orders","status":403,"title":"Forbidden","type":"https://ankurm.com/problems/access-denied"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Profile: catchall-first (spring-boot 4.1.1, spring-framework 7.0.9)
|
||||
|
||||
## domain exception (OrderNotFoundException)
|
||||
$ curl -X GET /orders/999
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders/999","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## type mismatch (/orders/abc)
|
||||
$ curl -X GET /orders/abc
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders/abc","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## invalid body (@Valid)
|
||||
$ curl -X POST /orders -d '{"sku":"","quantity":0,"customerEmail":"nope"}'
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## invalid @RequestParam (@Max)
|
||||
$ curl -X GET /orders?limit=500
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## ErrorResponseException (OutOfStock)
|
||||
$ curl -X POST /orders -d '{"sku":"SKU-2","quantity":3,"customerEmail":"[email protected]"}'
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## ResponseStatusException
|
||||
$ curl -X GET /orders/legacy/7
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders/legacy/7","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## unknown path
|
||||
$ curl -X GET /no-such-thing
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/no-such-thing","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## wrong HTTP method
|
||||
$ curl -X DELETE /orders/1
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders/1","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## malformed JSON
|
||||
$ curl -X POST /orders -d '{"sku":'
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## unexpected exception
|
||||
$ curl -X GET /orders/boom
|
||||
HTTP 500 Content-Type: application/problem+json
|
||||
{"detail":"Something went wrong","instance":"/orders/boom","status":500,"title":"Internal Server Error"}
|
||||
|
||||
## exception in a servlet filter
|
||||
$ curl -X GET /orders/1 [X-Tenant: BAD!]
|
||||
HTTP 500 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:37.596Z","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:37.638Z","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:37.845Z","status":403,"error":"Forbidden","path":"/admin/orders"}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Profile: defaults (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:04.029Z","status":500,"error":"Internal Server Error","path":"/orders/999"}
|
||||
|
||||
## type mismatch (/orders/abc)
|
||||
$ curl -X GET /orders/abc
|
||||
HTTP 400 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.063Z","status":400,"error":"Bad Request","path":"/orders/abc"}
|
||||
|
||||
## invalid body (@Valid)
|
||||
$ curl -X POST /orders -d '{"sku":"","quantity":0,"customerEmail":"nope"}'
|
||||
HTTP 400 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.250Z","status":400,"error":"Bad Request","path":"/orders"}
|
||||
|
||||
## invalid @RequestParam (@Max)
|
||||
$ curl -X GET /orders?limit=500
|
||||
HTTP 400 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.301Z","status":400,"error":"Bad Request","path":"/orders"}
|
||||
|
||||
## ErrorResponseException (OutOfStock)
|
||||
$ curl -X POST /orders -d '{"sku":"SKU-2","quantity":3,"customerEmail":"[email protected]"}'
|
||||
HTTP 409 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.354Z","status":409,"error":"Conflict","path":"/orders"}
|
||||
|
||||
## ResponseStatusException
|
||||
$ curl -X GET /orders/legacy/7
|
||||
HTTP 410 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.383Z","status":410,"error":"Gone","path":"/orders/legacy/7"}
|
||||
|
||||
## unknown path
|
||||
$ curl -X GET /no-such-thing
|
||||
HTTP 404 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.412Z","status":404,"error":"Not Found","path":"/no-such-thing"}
|
||||
|
||||
## wrong HTTP method
|
||||
$ curl -X DELETE /orders/1
|
||||
HTTP 405 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.437Z","status":405,"error":"Method Not Allowed","path":"/orders/1"}
|
||||
|
||||
## malformed JSON
|
||||
$ curl -X POST /orders -d '{"sku":'
|
||||
HTTP 400 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.462Z","status":400,"error":"Bad Request","path":"/orders"}
|
||||
|
||||
## unexpected exception
|
||||
$ curl -X GET /orders/boom
|
||||
HTTP 500 Content-Type: application/json
|
||||
{"timestamp":"2026-09-11T17:03:04.487Z","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:04.509Z","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:04.540Z","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:04.735Z","status":403,"error":"Forbidden","path":"/admin/orders"}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Which error shape does each failure produce? (status + body shape)
|
||||
# Spring Boot 4.1.1 / Spring Framework 7.0.9. Full bodies in matrix-<profile>.txt
|
||||
|
||||
failure | defaults | boot-flag | advice | advice,errors | catchall-first
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
domain exception (OrderNotFoundException) | 500 Boot /error JSON | 500 Boot /error JSON | 404 problem+json | 404 problem+json | 500 problem+json
|
||||
type mismatch (/orders/abc) | 400 Boot /error JSON | 400 problem+json | 400 problem+json | 400 problem+json | 500 problem+json
|
||||
invalid body (@Valid) | 400 Boot /error JSON | 400 problem+json | 400 problem+json | 400 problem+json | 500 problem+json
|
||||
invalid @RequestParam (@Max) | 400 Boot /error JSON | 400 problem+json | 400 problem+json | 400 problem+json | 500 problem+json
|
||||
ErrorResponseException (OutOfStock) | 409 Boot /error JSON | 409 problem+json | 409 problem+json | 409 problem+json | 500 problem+json
|
||||
ResponseStatusException | 410 Boot /error JSON | 410 problem+json | 410 problem+json | 410 problem+json | 500 problem+json
|
||||
unknown path | 404 Boot /error JSON | 404 problem+json | 404 problem+json | 404 problem+json | 500 problem+json
|
||||
wrong HTTP method | 405 Boot /error JSON | 405 problem+json | 405 problem+json | 405 problem+json | 500 problem+json
|
||||
malformed JSON | 400 Boot /error JSON | 400 problem+json | 400 problem+json | 400 problem+json | 500 problem+json
|
||||
unexpected exception | 500 Boot /error JSON | 500 Boot /error JSON | 500 problem+json | 500 problem+json | 500 problem+json
|
||||
exception in a servlet filter | 500 Boot /error JSON | 500 Boot /error JSON | 500 Boot /error JSON | 500 problem+json | 500 Boot /error JSON
|
||||
401 no credentials | 401 Boot /error JSON | 401 Boot /error JSON | 401 problem+json | 401 problem+json | 401 Boot /error JSON
|
||||
403 wrong role | 403 Boot /error JSON | 403 Boot /error JSON | 403 problem+json | 403 problem+json | 403 Boot /error JSON
|
||||
@@ -0,0 +1,15 @@
|
||||
# One request to an endpoint that throws IllegalStateException. What reaches the log?
|
||||
|
||||
defaults (Boot /error) ERROR lines: 1 stack frames: 87
|
||||
ERROR 30257 --- [problem-details] [omcat-handler-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request pr
|
||||
boot-flag ERROR lines: 1 stack frames: 87
|
||||
ERROR 30389 --- [problem-details] [omcat-handler-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request pr
|
||||
advice, catch-all logs with errorId ERROR lines: 1 stack frames: 87
|
||||
ERROR 30521 --- [problem-details] [omcat-handler-1] c.a.p.advice.GlobalExceptionHandler : Unhandled exception, errorId=0becc142-9239-44ba-93ae-c0d479052095
|
||||
advice, catch-all WITHOUT the log line ERROR lines: 0 stack frames: 0
|
||||
catchall-first (returns 500, never logs) ERROR lines: 0 stack frames: 0
|
||||
|
||||
# And for an exception thrown by a servlet filter (never reaches any advice):
|
||||
|
||||
advice,errors - filter exception ERROR lines: 1 stack frames: 68
|
||||
ERROR 30882 --- [problem-details] [omcat-handler-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
|
||||
@@ -0,0 +1,9 @@
|
||||
# ProblemDetail.forStatus(404).getType(), evaluated in jshell against each spring-web jar
|
||||
|
||||
spring-web-6.2.19.jar -> getType() = about:blank
|
||||
spring-web-7.0.9.jar -> getType() = null
|
||||
|
||||
# Both versions' ProblemDetailJacksonMixin carry @JsonInclude(NON_EMPTY), so 6.2 rendered
|
||||
# "type":"about:blank" and 7.0 omits the member. As rendered by this application (7.0.9):
|
||||
$ curl /no-such-thing
|
||||
{"detail":"No static resource no-such-thing.","instance":"/no-such-thing","status":404,"title":"Not Found"}
|
||||
Reference in New Issue
Block a user