fastapi-rfc9457
Typed, batteries-included RFC 9457 "Problem Details for HTTP APIs" for FastAPI & Pydantic.
Define an error once - it serializes as application/problem+json, documents itself in OpenAPI, and parses back into a typed exception on the client.
from fastapi import FastAPI
from fastapi_rfc9457 import Problem
from fastapi_rfc9457.server import add_problem_handlers, get_problem_docs_router, problems
class OutOfCredit(Problem):
"""The account does not have enough credit."""
title = "Out of Credit"
status = 403
balance: int # typed extension members, checked at the raise site
accounts: list[str]
class AccountSuspended(Problem):
"""The account is suspended and cannot be charged."""
title = "Account Suspended"
status = 403
app = FastAPI()
add_problem_handlers(app) # handlers + problem+json OpenAPI
app.include_router(get_problem_docs_router(), prefix="/problems") # dereferenceable type URIs
@app.get("/charge", responses=problems(OutOfCredit, AccountSuspended))
async def charge() -> dict:
raise OutOfCredit(detail="Not enough credit.", balance=30, accounts=["/acct/12"])
Accurate OpenAPI, for free
One route can declare several failure modes.
Distinct statuses get their own response; same-status problems become a oneOf union you flip through in Swagger's Examples dropdown — all under application/problem+json.
Dereferenceable type URIs
Mount the docs router and every problem type resolves to a live page listing its typed extension members.
The type follows the docs-router mount: mount at prefix="/problems" and OutOfCredit emits and serves /problems/out-of-credit.
Change the prefix and bodies, OpenAPI, and doc pages move together.
Set type on the class to emit a literal URI.
Response headers
A problem type declares the headers it sends in headers (name → OpenAPI description) and returns their values from response_headers().
The built-ins send the headers RFC 9110 asks for:
raise NotAuthenticated() # WWW-Authenticate: Bearer
raise TooManyRequests(retry_after=30) # Retry-After: 30
raise ServiceUnavailable(retry_after=120) # Retry-After: 120
raise MethodNotAllowed(allow=["GET"]) # Allow: GET
class BasicAuthRequired(NotAuthenticated):
challenge = 'Basic realm="api"' # WWW-Authenticate: Basic realm="api"
A custom problem type declares its own headers:
class Moved(Problem):
title, status = "Moved", 410
location: str
headers: ClassVar[Mapping[str, str]] = {"Location": "The new URL of the resource."}
def response_headers(self) -> Mapping[str, str]:
return {"Location": self.location}
Each class declares only its own headers, response_headers() and header_examples(); the library merges them with those of its bases, and sending an undeclared header emits UndeclaredHeaderWarning.
Shared bases
A class missing title or status raises TypeError when defined; mark shared bases abstract=True:
class Audited(Problem, abstract=True):
audit_id: str # every subclass carries it
class Throttled(RetryAfter): # built-in abstract base: retry_after + Retry-After
title, status = "Throttled", 429
Typed exceptions on the client
Client-side, the package can parse application/problem+json back into typed problems the server raised.
import httpx2
from fastapi_rfc9457 import Problem, httpx_raise_hook
class OutOfCredit(Problem): # the type the server declares, shared or re-stated
title = "Out of Credit"
status = 403
balance: int
with httpx2.Client(
base_url="http://localhost:8000",
event_hooks={
"response": [httpx_raise_hook()]
}) as client:
try:
client.get("/charge")
except OutOfCredit as exc:
print(exc.balance) # extension members round-trip back as typed attributes
Prefer to parse explicitly? parse_problem(response) returns the typed Problem (or a generic ProblemDetail for an unknown type), and raise_for_problem(response) raises it (ProblemError for an unknown type).
Comparison with native FastAPI
The same endpoint written the way FastAPI's Handling Errors tutorial shows:
class OutOfCreditError(Exception):
def __init__(self, detail: str, balance: int) -> None:
self.detail, self.balance = detail, balance
@app.exception_handler(OutOfCreditError)
async def _(request: Request, exc: OutOfCreditError) -> JSONResponse:
return JSONResponse({"detail": exc.detail, "balance": exc.balance}, 403)
class OutOfCreditBody(BaseModel):
detail: str
balance: int
@app.get("/charge", responses={403: {"model": OutOfCreditBody}})
async def charge(token: str | None = None) -> dict:
if token is None:
raise HTTPException(401, "Log in first")
raise OutOfCreditError("Not enough credit", balance=30)
# fastapi-rfc9457 enables a single class for the exception, the body, and the OpenAPI schema
from fastapi_rfc9457 import NotAuthenticated, Problem # NotAuthenticated ships built in
from fastapi_rfc9457.server import problems
class OutOfCredit(Problem):
title = "Out of Credit"
status = 403
balance: int
@app.get("/charge", responses=problems(NotAuthenticated, OutOfCredit))
async def charge(token: str | None = None) -> dict:
if token is None:
raise NotAuthenticated(detail="Log in first")
raise OutOfCredit(detail="Not enough credit", balance=30)
# → 403 application/problem+json
# {"type": "/problems/out-of-credit", "title": "Out of Credit",
# "status": 403, "detail": "Not enough credit", "balance": 30}
| Plain FastAPI | fastapi-rfc9457 | |
|---|---|---|
| Typed extra fields in the body and OpenAPI | exception + handler + model, by hand | ✅ |
Errors documented as application/problem+json |
❌ (application/json) |
✅ |
Same-status errors as oneOf + Examples dropdown |
❌ | ✅ |
Dereferenceable type URIs with doc pages |
❌ | ✅ |
Similar projects
Install
uv add fastapi-rfc9457[server] # FastAPI apps: handlers, OpenAPI, docs router
uv add fastapi-rfc9457 # lean client: author + parse problems, Pydantic only
Example
cd example && uv run uvicorn main:app --reload # then open localhost:8000/docs
See example/ for the full app and example/client.py for a client using it.
Notes
- Every error answers with
application/problem+json: raised problems,HTTPException, request validation (422), and unhandled exceptions (500). - 500 bodies include the exception message by default.
Pass
add_problem_handlers(app, strip_debug=True)in production to redact it and the offending input on 422s.
Release files for fastapi-rfc9457 0.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_rfc9457-0.2.3.tar.gz | 38.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_rfc9457-0.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.4 kB
Release files / fastapi_rfc9457-0.2.3.tar.gz
| Download URL | fastapi_rfc9457-0.2.3.tar.gz |
|---|---|
| Size | 38.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
97bea471cd7959a10efb63106c3f7d071f549e976fe79a536c70f9f42eeb2a63
|
|
BLAKE2b-256 checksum How to use checksums |
2ae5fd408a6a7c99f5906115b7d009ba68529a8f1da6baefa8faa71f0f564ab4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / fastapi_rfc9457-0.2.3-py3-none-any.whl
| Download URL | fastapi_rfc9457-0.2.3-py3-none-any.whl |
|---|---|
| Size | 27.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6f08384f70f49074f91566cc1a272570b37adf507448573e708afcb446162276
|
|
BLAKE2b-256 checksum How to use checksums |
938c7940daf2bf5b8523e9ad57964dcb1c9a0394a50f738fdf32481ef0237fb3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log