122 lines
4.4 KiB
Markdown
122 lines
4.4 KiB
Markdown
# 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:
|
||
|
||
```text
|
||
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
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python
|
||
OpenAPIClient(spec) # fails fast, same philosophy
|
||
```
|
||
|
||
- Spec with no `servers` → `OpenAPIClientError`
|
||
- Spec with no `paths` → `OpenAPIClientError`
|
||
- Duplicate `operationId`s → `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:
|
||
|
||
```python
|
||
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:
|
||
|
||
```python
|
||
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
|
||
|
||
```python
|
||
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.
|
||
|
||
---
|
||
|
||
## Related
|
||
|
||
- [01 – Overview](01_overview.md) · [05 – Security](05_security.md) · [07 – Testing](07_testing.md)
|