Files
openapi-first/docs/wiki/06_error_handling.md

4.4 KiB
Raw Permalink Blame History

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 serversOpenAPIClientError
  • Spec with no pathsOpenAPIClientError
  • Duplicate operationIds → OpenAPIClientError (client methods must be unambiguous)
  • Operation missing operationIdOpenAPIClientError

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.