Короткий ответ
Для каждого типа исключений регистрируется обработчик через @app.exception_handler(ТипИсключения), который формирует ответ в едином JSON-формате; для RequestValidationError и собственных исключений (например, DomainError) пишутся отдельные обработчики, возвращающие согласованную структуру.
Как это работает подробнее
FastAPI/Starlette вызывают зарегистрированный обработчик, соответствующий типу выброшенного исключения (с учётом наследования), вместо стандартного поведения. Внутри обработчика формируется JSONResponse с нужным статусом и телом, одинаковым по структуре для всех типов ошибок приложения.
Пример кода
example.py
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
class AppError(Exception):
status_code = 400
def __init__(self, message: str):
self.message = message
class NotFoundError(AppError):
status_code = 404
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(status_code=exc.status_code, content={"error": {"message": exc.message}})
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(status_code=422, content={"error": {"message": "validation_failed", "details": exc.errors()}})Что использовать на практике
- Практика. Полезно завести базовый класс исключений домена (
class AppError(Exception)) с подклассами (NotFoundError,ConflictError) и статус-кодом как атрибутом, чтобы обработчик был один и обобщённый, а не по одному на каждый тип ошибки.
Подводные камни
- Риск. Регистрируют обработчик только для конкретных исключений, забывая про общий
Exception(нужно аккуратно — слишком widely пойманныйExceptionможет скрыть реальные баги и превратить их в «красивый» 500 без алерта); делают формат ответа непоследовательным между разными обработчиками (где-тоdetail, где-тоmessage), усложняя жизнь клиентам API.