Некоторая логика нужна всем запросам сразу: измерить время ответа, поставить идентификатор для отладки, превратить исключение в понятный JSON. Дублировать это в каждом эндпоинте — значит неизбежно про что-то забыть. FastAPI даёт два инструмента для такой сквозной логики: middleware и обработчики исключений.
Middleware — логика до и после эндпоинта
Представьте, что каждый запрос проходит сквозь несколько слоёв-«оберток», прежде чем добраться до вашего кода, и возвращается обратно сквозь те же слои. Каждый слой — это middleware.
Простейший пример — замер времени обработки запроса:
import time
from fastapi import Request
@app.middleware("http")
async def add_timing(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.3f}"
return response
Здесь call_next — это «передать запрос дальше». Всё до него выполняется до эндпоинта, всё после — когда ответ уже готов. Middleware может читать и изменять и запрос, и ответ.
Порядок middleware
Middleware регистрируются в стек: последняя зарегистрированная обрабатывает запрос первой. То, что должно охватывать всё — логирование, идентификатор запроса — регистрируют последним, чтобы оно оборачивало всё остальное.
app.add_middleware(TimingMiddleware)
app.add_middleware(RequestIdMiddleware) # выполнится первым
Middleware работает на каждом запросе, поэтому тяжёлую логику туда не кладут.
Идентификатор запроса
Частый и полезный паттерн: ставить уникальный request_id на входе, чтобы потом связать все логи одного запроса:
import uuid
from fastapi import Request
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
Обработчики исключений — единый формат ошибок
Без централизованной обработки каждый эндпоинт сам превращает ошибку в JSON-ответ. Когда эндпоинтов десятки, формат расходится.
Решение — зарегистрировать обработчик на тип исключения. FastAPI применит его везде автоматически:
from fastapi import Request
from fastapi.responses import JSONResponse
class ProductNotFound(Exception):
def __init__(self, product_id: int):
self.product_id = product_id
@app.exception_handler(ProductNotFound)
async def product_not_found_handler(request: Request, exc: ProductNotFound):
return JSONResponse(
status_code=404,
content={"error": "product_not_found", "product_id": exc.product_id},
)
Теперь эндпоинт просто бросает ProductNotFound, ничего не зная про HTTP — а обработчик превращает это в 404 с нужным телом.
HTTPException и ошибки валидации
FastAPI обрабатывает два типа ошибок из коробки, их полезно понимать.
HTTPException — быстрый способ вернуть ошибку прямо из кода:
from fastapi import HTTPException
@router.get("/{product_id}")
async def get_product(product_id: int):
product = await repo.find(product_id)
if product is None:
raise HTTPException(status_code=404, detail="not found")
return product
RequestValidationError FastAPI бросает сам, когда тело или параметры не прошли Pydantic-проверку, и возвращает 422. Если нужен единый формат ошибок по всему API, его переопределяют так же, как любое другое исключение:
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"error": "validation_failed", "details": exc.errors()},
)
Middleware или зависимость — что выбрать
Middleware и Depends выглядят похоже, но решают разные задачи.
Middleware видит каждый запрос без исключения и работает на уровне HTTP-транспорта: логи, тайминги, заголовки, идентификатор запроса.
Зависимость (Depends) подключается только к тем эндпоинтам, которым нужна, и умеет собирать объекты. Текущий пользователь, проверка токена, открытие сессии базы данных — это задачи для зависимости, не для middleware.
Проверку прав на конкретный роут делают зависимостью. Добавить заголовок ко всем ответам — middleware.
Коротко
- Middleware оборачивает каждый запрос: код до
call_nextвыполняется на входе, после — на выходе. - Последняя зарегистрированная middleware выполняется первой — регистрируй логирование последним.
@app.exception_handler(SomeError)перехватывает исключение по всему приложению — единый формат ошибок в одном месте.HTTPException— быстрый способ вернуть HTTP-ошибку из кода;RequestValidationError— ошибка Pydantic-валидации, бросается автоматически.- Middleware — для сквозного (все запросы, транспортный уровень); зависимость — для конкретных эндпоинтов.
Что почитать дальше
- Dependency Injection и Depends — зависимости, текущий пользователь, сессия БД.
- Валидация с Pydantic — откуда берётся
RequestValidationError. - Наблюдаемость FastAPI — как идентификатор запроса из middleware связывает логи и трейсы.