Skip to main content

fastapi-faults

Typed error contracts for FastAPI — from Python exceptions to RFC 9457 and OpenAPI.

CI Python 3.12–3.14 FastAPI RFC 9457 License: MIT

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)

Source distribution for fastapi-faults 0.1.0
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)

Table of built distributions (wheels) for fastapi-faults 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page