← назад к разделу

Некоторая логика нужна всем запросам сразу: измерить время ответа, поставить идентификатор для отладки, превратить исключение в понятный 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 связывает логи и трейсы.