chore(release): 0.0.7
Squash-merge lib-standardisation: adopt repo standards across packaging, tooling, docs, and CI. - standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint) - add py.typed marker, modernize type hints and Google-style docstrings - nest generated docs under docs/lib and docs/mcp; add hand-written wiki - adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate) - promote generated CHANGELOG.md; bump version to 0.0.7
This commit is contained in:
121
docs/wiki/06_error_handling.md
Normal file
121
docs/wiki/06_error_handling.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user