4.4 KiB
Error Handling — Fail Loud, Fail Early
openapi-first treats errors as first-class contract documents: every failure mode is a named exception with a stable import pathhare, and every one surfaces as early as possible.
🧬 1. The Hierarchy
All errors derive from OpenAPIFirstError (in openapi_first/errors.py), so a single except OpenAPIFirstError catches every first-party failure:
OpenAPIFirstError
├── OpenAPISpecError # spec-level problems
│ └── OpenAPISpecLoadError # load / parse / validation (loader)
├── OpenAPIClientError # client-side contract issues (client)
└── MissingOperationHandler # spec op with no handler (binder)
# Security / loader layers raise through OpenAPISpecError subclasses too
| Exception | Module | Raised when |
|---|---|---|
OpenAPISpecLoadError |
loader |
Path missing, file unreadable, YAML/JSON invalid, or spec fails OpenAPI 3.x validation |
OpenAPIClientError |
client |
No servers, no paths, missing/duplicate operationId, missing required params at construction |
MissingOperationHandler |
errors |
An operation is declared whose operationId has no matching handler in routes_module |
⏱️ 2. When Things Fail
The single most important rule: violations are eager, not lazy.
2.1 At application startup
# openapi.yaml missing ────────────────────────────► OpenAPISpecLoadError
# operationId without a handler ───────────────────► MissingOperationHandler
# operation with no operationId declared ──────────► MissingOperationHandler
Because these raise during OpenAPIFirstApp(...) construction, CI catches them the moment a spec and its routes drift — before a single request is served.
2.2 At client construction
OpenAPIClient(spec) # fails fast, same philosophy
- Spec with no
servers→OpenAPIClientError - Spec with no
paths→OpenAPIClientError - Duplicate
operationIds →OpenAPIClientError(client methods must be unambiguous) - Operation missing
operationId→OpenAPIClientError
2.3 At call time (client)
Runtime transport errors surface as httpx exceptions (httpx.RequestError family), not swallowed or remapped. Missing required args fail before any HTTP request is made:
client.get_user(path_params={"user_id": ...}) # OK
client.get_user() # ValueError — user_id required
🧰 3. Handling in Your App
3.1 Server-side
Handlers raise FastAPI HTTPException for expected operation-level failures (404/422), and the OperationId-binding errors only exist at startup:
from fastapi import HTTPException
def get_item(item_id: int):
"""Retrieve an item by ID.
Implements the OpenAPI operation ``get_item``.
Args:
item_id (int): Identifier of the item.
Raises:
HTTPException: If the item does not exist (404).
"""
try:
return _get_item(item_id)
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
3.2 Client-side
import httpx
try:
response = client.get_item(path_params={"item_id": 1})
except httpx.HTTPStatusError as exc:
... # 4xx/5xx from the server
httpx.HTTPStatusError isn't raised by the library — it's the standard httpx.raise_for_status() you can opt into per call. The library never masks a response code.
🛡️ 4. Fail-Fast Guarantees Recap
| Layer | You write | The library guarantees |
|---|---|---|
| Loader | a spec path | unreadable/invalid specs never reach your app |
| Binder | handler functions | every operation must resolve, or the app won't start |
| Client | a spec | every operationId becomes a callable; missing ones fail at construction |
| Runtime | handler code | FastAPI + Pydantic handle coercion; contract checks already happened at startup |
None of these can silently degrade: a violation is an exception at construction, not a 500 at request time.