API Error Handling

Error response patterns, status code decisions, and retry strategies.

Error Response Patterns

Standard error envelope — consistent structure across APIs
{
  "error": {
    "code": 422,
    "message": "Validation failed",
    "details": [
      {"field": "vlan", "issue": "must be between 1 and 4094"},
      {"field": "name", "issue": "already exists"}
    ],
    "request_id": "req-abc123"
  }
}
FastAPI HTTPException pattern — domus-api style
from fastapi import HTTPException

raise HTTPException(
    status_code=404,
    detail={"message": "Device not found", "device_id": device_id}
)
FastAPI validation error — Pydantic rejects invalid input automatically
{
  "detail": [
    {
      "type": "int_parsing",
      "loc": ["body", "vlan"],
      "msg": "Input should be a valid integer",
      "input": "not-a-number"
    }
  ]
}

HTTP Status Code Decision Tree

When to use which error code
Is the request malformed?
├── Yes → 400 Bad Request (missing field, wrong type)
└── No → Is the client authenticated?
    ├── No → 401 Unauthorized (missing/invalid credentials)
    └── Yes → Is the client authorized?
        ├── No → 403 Forbidden (valid creds, insufficient perms)
        └── Yes → Does the resource exist?
            ├── No → 404 Not Found
            └── Yes → Is there a conflict?
                ├── Yes → 409 Conflict (duplicate, version mismatch)
                └── No → Is input semantically valid?
                    ├── No → 422 Unprocessable Entity
                    └── Yes → 500 Internal Server Error

Client-Side Error Handling

Bash — check status code and parse error body
response=$(curl -s -w '\n%{http_code}' \
  -H "Authorization: Bearer $TOKEN" \
  https://api.example.com/v1/devices/999)

status=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')

case "$status" in
    200) echo "$body" | jq . ;;
    401) echo "Auth failed -- token expired?" >&2; exit 1 ;;
    404) echo "Device not found" >&2; exit 1 ;;
    429) echo "Rate limited -- retry after $(echo "$body" | jq -r '.retry_after // 60')s" >&2 ;;
    5??) echo "Server error ($status) -- retry later" >&2; exit 2 ;;
    *)   echo "Unexpected: HTTP $status" >&2; echo "$body" | jq . >&2 ;;
esac
Python httpx — structured error handling
import httpx

try:
    response = httpx.get("https://api.example.com/v1/devices/42",
                         headers={"Authorization": f"Bearer {token}"})
    response.raise_for_status()
    device = response.json()
except httpx.HTTPStatusError as e:
    if e.response.status_code == 404:
        print(f"Device not found: {e.response.json()['detail']}")
    elif e.response.status_code == 429:
        retry_after = int(e.response.headers.get("Retry-After", 60))
        print(f"Rate limited, retry after {retry_after}s")
    else:
        print(f"HTTP {e.response.status_code}: {e.response.text}")
except httpx.ConnectError:
    print("Cannot reach API -- network issue or DNS failure")
except httpx.TimeoutException:
    print("Request timed out")

Retry Strategies

Exponential backoff with jitter — prevent thundering herd
import httpx
import time
import random

def request_with_retry(url: str, max_retries: int = 5) -> httpx.Response:
    for attempt in range(max_retries):
        try:
            response = httpx.get(url, timeout=10.0)
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
                time.sleep(retry_after + random.uniform(0, 1))
                continue
            response.raise_for_status()
            return response
        except httpx.TransportError:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt + random.uniform(0, 1))
    raise RuntimeError(f"Failed after {max_retries} attempts")
Idempotency key — safe retries for POST
idempotency_key=$(uuidgen)
for attempt in 1 2 3; do
    status=$(curl -s -o /dev/null -w '%{http_code}' \
      -X POST https://api.example.com/v1/orders \
      -H "Idempotency-Key: $idempotency_key" \
      -H "Content-Type: application/json" \
      -d '{"item": "license"}')
    [[ "$status" -eq 201 || "$status" -eq 200 ]] && break
    sleep $(( 2 ** attempt ))
done

ISE ERS Error Patterns

ISE ERS returns XML errors by default — always request JSON
# Wrong -- gets XML error you cannot easily parse
curl -sk -u "$ISE_USER:$ISE_PASS" \
  https://10.50.1.20:9060/ers/config/endpoint/bad-id

# Correct -- JSON error response
curl -sk -u "$ISE_USER:$ISE_PASS" \
  -H "Accept: application/json" \
  https://10.50.1.20:9060/ers/config/endpoint/bad-id | jq .
Common ISE ERS errors
401 -- ERS admin credentials wrong or ERS API not enabled
404 -- Resource ID does not exist
500 -- ISE internal error (check ise-psc.log on the node)