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.

[!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

fastapi_faults-0.1.0.tar.gz (102.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_faults-0.1.0-py3-none-any.whl (22.6 kB view details)

Uploaded Python 3

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

Hashes for fastapi_faults-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b86a2699f1186a5f3f4acbc89278511ec9c50286c88bcbc4ac1ac74793132b4f
MD5 add786ec1caf4704f6ad1bbe22c159f8
BLAKE2b-256 102c480621d9d1ecc505f85811cfda7ae67a6c70a99edbaf0159fdba85eddfb2

See more details on using hashes here.

File details

Details for the file fastapi_faults-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_faults-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 12f782b836d95472fff850a1f66623966ff71eda0af2b8cc61bda4533de4af1a
MD5 bca1c397db37ad0e664c02e362b869aa
BLAKE2b-256 6cff87ce28fd57674111b836707938f26469cc65cb71c77658732119e97fd4f4

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

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