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.
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.
Metadata
Release files for fastapi-faults 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| fastapi_faults-0.1.0.tar.gz | 102.0 kB | Without provenance | Details for fastapi_faults-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| fastapi_faults-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for fastapi_faults-0.1.0-py3-none-any.whl |
Total release size: 124.6 kB
Release files / fastapi_faults-0.1.0.tar.gz
| Download URL | fastapi_faults-0.1.0.tar.gz |
|---|---|
| Size | 102.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b86a2699f1186a5f3f4acbc89278511ec9c50286c88bcbc4ac1ac74793132b4f
|
|
BLAKE2b-256 checksum How to use checksums |
102c480621d9d1ecc505f85811cfda7ae67a6c70a99edbaf0159fdba85eddfb2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / fastapi_faults-0.1.0-py3-none-any.whl
| Download URL | fastapi_faults-0.1.0-py3-none-any.whl |
|---|---|
| Size | 22.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12f782b836d95472fff850a1f66623966ff71eda0af2b8cc61bda4533de4af1a
|
|
BLAKE2b-256 checksum How to use checksums |
6cff87ce28fd57674111b836707938f26469cc65cb71c77658732119e97fd4f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Security for fastapi-faults 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.