5.1 KiB
Exceptions
Structured API exceptions with consistent error responses and automatic OpenAPI documentation.
Overview
The exceptions module provides a set of pre-built HTTP exceptions and a FastAPI exception handler that formats all errors — including validation errors — into a uniform ErrorResponse.
Setup
Register the exception handlers on your FastAPI app at startup:
from fastapi import FastAPI
from fastapi_toolsets.exceptions import init_exceptions_handlers
app = FastAPI()
init_exceptions_handlers(app=app)
This registers handlers for:
ApiException— all custom exceptions belowHTTPException— Starlette/FastAPI HTTP errorsRequestValidationError— Pydantic request validation (422)ResponseValidationError— Pydantic response validation (422)Exception— unhandled errors (500)
It also patches app.openapi() to replace the default Pydantic 422 schema with a structured example matching the ErrorResponse format.
Built-in exceptions
| Exception | Status | Default message |
|---|---|---|
UnauthorizedError |
401 | Unauthorized |
ForbiddenError |
403 | Forbidden |
NotFoundError |
404 | Not Found |
ConflictError |
409 | Conflict |
NoSearchableFieldsError |
400 | No Searchable Fields |
InvalidFacetFilterError |
400 | Invalid Facet Filter |
InvalidOrderFieldError |
422 | Invalid Order Field |
Per-instance overrides
All built-in exceptions accept optional keyword arguments to customise the response for a specific raise site without changing the class defaults:
| Argument | Effect |
|---|---|
detail |
Overrides both str(exc) (log output) and the message field in the response body |
desc |
Overrides the description field |
data |
Overrides the data field |
raise NotFoundError(detail="User 42 not found", desc="No user with that ID exists in the database.")
Custom exceptions
Subclass ApiException and define an api_error class variable:
from fastapi_toolsets.exceptions import ApiException
from fastapi_toolsets.schemas import ApiError
class PaymentRequiredError(ApiException):
api_error = ApiError(
code=402,
msg="Payment Required",
desc="Your subscription has expired.",
err_code="BILLING-402",
)
!!! warning
Subclasses that do not define api_error raise a TypeError at class creation time, not at raise time.
Custom __init__
Override __init__ to compute detail, desc, or data dynamically, then delegate to super().__init__():
class OrderValidationError(ApiException):
api_error = ApiError(
code=422,
msg="Order Validation Failed",
desc="One or more order fields are invalid.",
err_code="ORDER-422",
)
def __init__(self, *field_errors: str) -> None:
super().__init__(
f"{len(field_errors)} validation error(s)",
desc=", ".join(field_errors),
data={"errors": [{"message": e} for e in field_errors]},
)
Intermediate base classes
Use abstract=True when creating a shared base that is not meant to be raised directly:
class BillingError(ApiException, abstract=True):
"""Base for all billing-related errors."""
class PaymentRequiredError(BillingError):
api_error = ApiError(code=402, msg="Payment Required", desc="...", err_code="BILLING-402")
class SubscriptionExpiredError(BillingError):
api_error = ApiError(code=402, msg="Subscription Expired", desc="...", err_code="BILLING-402-EXP")
OpenAPI response documentation
Use generate_error_responses to add error schemas to your endpoint's OpenAPI spec:
from fastapi_toolsets.exceptions import generate_error_responses, NotFoundError, ForbiddenError
@router.get(
"/users/{id}",
responses=generate_error_responses(NotFoundError, ForbiddenError),
)
async def get_user(...): ...
Multiple exceptions sharing the same HTTP status code are grouped under one entry, each appearing as a named example keyed by its err_code. This keeps the OpenAPI UI readable when several error variants map to the same status.