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
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# The article's first table: the same thirteen failures under each exception-handling setup.
# Writes docs/output/matrix-<profile>.txt (full responses) and docs/output/matrix-summary.txt.
set -uo pipefail
source "$(dirname "$0")/env.sh"
SCENARIOS=(
"domain exception (OrderNotFoundException)|GET|/orders/999||"
"type mismatch (/orders/abc)|GET|/orders/abc||"
"invalid body (@Valid)|POST|/orders|{\"sku\":\"\",\"quantity\":0,\"customerEmail\":\"nope\"}|"
"invalid @RequestParam (@Max)|GET|/orders?limit=500||"
"ErrorResponseException (OutOfStock)|POST|/orders|{\"sku\":\"SKU-2\",\"quantity\":3,\"customerEmail\":\"[email protected]\"}|"
"ResponseStatusException|GET|/orders/legacy/7||"
"unknown path|GET|/no-such-thing||"
"wrong HTTP method|DELETE|/orders/1||"
"malformed JSON|POST|/orders|{\"sku\":|"
"unexpected exception|GET|/orders/boom||"
"exception in a servlet filter|GET|/orders/1||X-Tenant: BAD!"
"401 no credentials|GET|/admin/orders||"
"403 wrong role|GET|/admin/orders||AUTH:user:user"
)
classify() { # content-type, body -> shape label
local ct="$1" body="$2"
if [[ "$ct" == *problem+json* ]]; then echo "problem+json"
elif [[ -z "$body" ]]; then echo "(empty body)"
elif [[ "$body" == *'"timestamp"'* && "$body" == *'"error"'* ]]; then echo "Boot /error JSON"
else echo "other: ${ct:-none}"; fi
}
SUMMARY="$OUT/matrix-summary.txt"
PROFILES=("" "boot-flag" "advice" "advice,errors" "catchall-first")
declare -A CELL
for profile in "${PROFILES[@]}"; do
name="${profile:-defaults}"
"$MODULE_DIR/scripts/run.sh" "$profile" || exit 1
file="$OUT/matrix-$name.txt"
{
echo "# Profile: $name (spring-boot 4.1.1, spring-framework 7.0.9)"
echo
} > "$file"
i=0
for s in "${SCENARIOS[@]}"; do
IFS='|' read -r label method path body extra <<< "$s"
args=(-s -o /tmp/pd-body -D /tmp/pd-headers -X "$method" "$BASE$path")
[ -n "$body" ] && args+=(-H 'Content-Type: application/json' --data "$body")
if [[ "$extra" == AUTH:* ]]; then args+=(-u "${extra#AUTH:}");
elif [ -n "$extra" ]; then args+=(-H "$extra"); fi
curl "${args[@]}"
status=$(head -1 /tmp/pd-headers | awk '{print $2}')
ct=$(grep -i '^content-type:' /tmp/pd-headers | head -1 | cut -d' ' -f2- | tr -d '\r')
resp=$(cat /tmp/pd-body)
{
echo "## $label"
echo "\$ curl -X $method $path${body:+ -d '$body'}${extra:+ [$extra]}"
echo "HTTP $status Content-Type: ${ct:-<none>}"
[ -n "$resp" ] && echo "$resp"
echo
} >> "$file"
CELL["$i|$name"]="$status $(classify "$ct" "$resp")"
i=$((i+1))
done
done
"$MODULE_DIR/scripts/stop.sh"
{
echo "# Which error shape does each failure produce? (status + body shape)"
echo "# Spring Boot 4.1.1 / Spring Framework 7.0.9. Full bodies in matrix-<profile>.txt"
echo
printf '%-42s | %-22s | %-22s | %-22s | %-22s | %-22s\n' "failure" "defaults" "boot-flag" "advice" "advice,errors" "catchall-first"
printf '%s\n' "$(printf -- '-%.0s' {1..165})"
i=0
for s in "${SCENARIOS[@]}"; do
label="${s%%|*}"
printf '%-42s | %-22s | %-22s | %-22s | %-22s | %-22s\n' "$label" "${CELL["$i|defaults"]}" "${CELL["$i|boot-flag"]}" "${CELL["$i|advice"]}" "${CELL["$i|advice,errors"]}" "${CELL["$i|catchall-first"]}"
i=$((i+1))
done
} > "$SUMMARY"
cat "$SUMMARY"