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
80 lines
3.2 KiB
Bash
80 lines
3.2 KiB
Bash
#!/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"
|