fastapi-faults
Typed error contracts for FastAPI — from Python exceptions to RFC 9457 and OpenAPI.
fastapi-faults gives every application error one immutable definition and
uses it everywhere: exception handling, application/problem+json responses,
and generated OpenAPI documentation.
domain exception ──▶ Fault ──▶ runtime response
└──▶ OpenAPI schema
No duplicated responses={...} dictionaries, no process-global registry, and
no drift between what an endpoint documents and what it actually returns.
[!IMPORTANT] The project is currently pre-release. The public contract is taking shape, but breaking changes are still possible before 1.0.
Why fastapi-faults?
- One source of truth — status, stable code, title, detail, headers, examples,
and schemas live in one
Fault. - Real Problem Details — errors use the RFC 9457 media type and structure.
- OpenAPI that stays honest —
responses=registry.responses(...)produces the matching response documentation automatically. - Typed extension members — Pydantic models validate custom problem fields and generate their schemas.
- Feature-local design — define faults beside a feature, then compose registries at the application boundary.
- Safe defaults — request validation, FastAPI HTTP errors, and unexpected failures can be normalized without exposing private inputs or internals.
- Contract testing included — assert response shape, OpenAPI coverage, and undeclared runtime faults.
Quickstart
Install the current development version from GitHub:
uv add "fastapi-faults @ git+https://github.com/mathisarends/fastapi_faults.git"
Or with pip:
python -m pip install "fastapi-faults @ git+https://github.com/mathisarends/fastapi_faults.git"
Define a domain exception, map it once, and declare it on the route that can raise it:
from fastapi import APIRouter, FastAPI
from fastapi_faults import Fault, FaultRegistry
class SessionNotFound(Exception):
def __init__(self, session_id: str) -> None:
self.session_id = session_id
SESSION_NOT_FOUND = Fault(
SessionNotFound,
status=404,
code="session_not_found",
title="Session not found",
detail=lambda error: f"Session {error.session_id} does not exist.",
)
session_faults = FaultRegistry(
name="sessions",
faults=[SESSION_NOT_FOUND],
)
router = APIRouter(prefix="/sessions", tags=["sessions"])
@router.get(
"/{session_id}",
responses=session_faults.responses(SESSION_NOT_FOUND),
)
async def get_session(session_id: str) -> dict[str, str]:
raise SessionNotFound(session_id)
app = FastAPI()
app.include_router(router)
api_faults = FaultRegistry.merge(
session_faults,
name="api",
type_base="https://api.example.com/problems",
)
api_faults.install(app)
A request to GET /sessions/abc now returns:
HTTP/1.1 404 Not Found
content-type: application/problem+json
{
"type": "https://api.example.com/problems/session_not_found",
"title": "Session not found",
"status": 404,
"code": "session_not_found",
"detail": "Session abc does not exist."
}
The same route is documented in OpenAPI with a 404 response using
application/problem+json and a reusable SessionNotFoundProblem schema.
How it fits together
Keep errors close to their feature
Each feature owns a small registry. The application composes them explicitly:
api_faults = FaultRegistry.merge(
browser_faults,
session_faults,
account_faults,
name="api",
type_base="https://api.example.com/problems",
)
Definitions are immutable, merge order is deterministic, and conflicting exception classes, codes, type URIs, or schema names fail during configuration.
type_base derives a stable problem type URI from each fault's code. A
fault can instead provide an explicit type. Installation fails when a domain
fault has neither, keeping incomplete contracts out of a running application.
Add typed problem fields
RFC 9457 allows problem-specific extension members at the top level. Use a Pydantic model to validate those values and describe them in OpenAPI:
from pydantic import BaseModel
class ConflictFields(BaseModel):
current_version: int
SESSION_CONFLICT = Fault(
SessionConflict,
status=409,
code="session_conflict",
title="Session conflict",
extensions_model=ConflictFields,
extensions=lambda error: ConflictFields(current_version=error.version),
)
This produces a top-level current_version member at runtime and an integer
property in the generated problem schema.
Declare route faults
Use FastAPI's standard APIRouter and generate its responses metadata from
the feature registry:
@router.get(
"/{session_id}",
responses=session_faults.responses(SESSION_NOT_FOUND),
)
async def get_session(session_id: str) -> SessionView:
...
This is the canonical route API. fastapi-faults does not subclass or replace
APIRouter. Every declared fault must belong to the registry installed on the
application.
Framework errors and safe fallbacks
Installing a registry normalizes FastAPI and Starlette failures by default:
| Failure | Default behavior |
|---|---|
| Request validation | 422 Problem Details with stable, location-aware errors |
HTTPException / routing errors |
Matching Problem Details response |
| Response validation | Safe internal-error response |
| Unexpected exception | Safe internal-error response |
Built-in handlers can be selected at installation time:
api_faults.install(
app,
include_validation_error=True,
include_http_exceptions=True,
include_unhandled_error=True,
)
Testing contracts
The repository's own test helpers live in tests/helpers.py and are not shipped
with the library. Within this checkout, they can check runtime and documentation
drift:
from tests.helpers import (
assert_no_undeclared_faults,
assert_openapi_contract,
assert_problem,
)
assert_openapi_contract(app)
response = client.get("/sessions/abc")
problem = assert_problem(
response,
SESSION_NOT_FOUND,
type_uri="https://api.example.com/problems/session_not_found",
)
assert problem["detail"] == "Session abc does not exist."
async with assert_no_undeclared_faults(app):
# Exercise routes here. The context fails afterward if a registered fault
# occurred without being declared in that operation's responses metadata.
...
The undeclared-fault monitor must be entered before the application's first request.
Requirements
- CPython 3.12, 3.13, or 3.14
- FastAPI 0.115 or newer (below 1.0)
- Pydantic 2.9 or newer (below 3.0)
Development
Clone the repository, then install all dependency groups:
uv sync --all-groups
Run the same quality gates used by the project:
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pytest
uv build
Runnable samples live in examples/minimal and the
feature-oriented examples/namespaced showcase.
License
Released under the MIT License.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_faults-0.1.0.tar.gz.
File metadata
- Download URL: fastapi_faults-0.1.0.tar.gz
- Upload date:
- Size: 102.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b86a2699f1186a5f3f4acbc89278511ec9c50286c88bcbc4ac1ac74793132b4f
|
|
| MD5 |
add786ec1caf4704f6ad1bbe22c159f8
|
|
| BLAKE2b-256 |
102c480621d9d1ecc505f85811cfda7ae67a6c70a99edbaf0159fdba85eddfb2
|
File details
Details for the file fastapi_faults-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_faults-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12f782b836d95472fff850a1f66623966ff71eda0af2b8cc61bda4533de4af1a
|
|
| MD5 |
bca1c397db37ad0e664c02e362b869aa
|
|
| BLAKE2b-256 |
6cff87ce28fd57674111b836707938f26469cc65cb71c77658732119e97fd4f4
|