Middleware и порядок обработки
Код, который должен выполняться для каждого запроса — логирование, замер времени, заголовки безопасности, — пишут не в обработчиках, а в middleware.
Middleware — это слой, оборачивающий обработку запроса: он получает каждый входящий запрос до того, как тот дойдёт до эндпоинта, и каждый ответ после того, как эндпоинт отработал, — и может изменить любой из них.
Представьте, что к каждому ответу нужно добавить заголовок со временем обработки, а каждый запрос — записать в лог. Дописывать это в сотню эндпоинтов нелепо и ненадёжно. Middleware выполняется централизованно для всего трафика: один раз написали — действует везде. На middleware держатся CORS, сжатие ответов, аутентификация по токену, ограничение частоты запросов, корреляционные ID для трассировки.
Своя middleware
Простейший способ — декоратор @app.middleware("http"). Функция получает request и call_next — корутину, которая передаёт управление дальше по цепочке и возвращает ответ:
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request) # дальше по цепочке к эндпоинту
elapsed = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{elapsed:.4f}"
return response
Всё, что до await call_next(request) — выполняется на входе (можно проверить запрос, начать таймер). Всё, что после — на выходе (можно дополнить ответ). Здесь мы измеряем время обработки и кладём его в заголовок X-Process-Time, который увидит любой клиент. Важно обязательно вернуть response: если этого не сделать, клиент не получит ответ вовсе.
Тот же приём — основа сквозного логирования. На входе генерируют корреляционный идентификатор запроса, кладут его в заголовок ответа и в каждую строку лога; тогда по этому ID можно собрать всю историю одного запроса, даже если он прошёл через несколько сервисов. Логировать в middleware удобно именно потому, что точка одна: метод, путь, итоговый статус и длительность пишутся централизованно, а не повторяются в каждом обработчике. Эндпоинты при этом остаются чистыми — они занимаются только бизнес-логикой, а инфраструктурные заботы (тайминг, заголовки, лог) сосредоточены в слое middleware и применяются ко всему трафику одинаково.
Порядок выполнения
Это место, где ошибаются чаще всего. Middleware образуют «луковицу»: добавленная последней оборачивает добавленные раньше. Запрос проходит слои снаружи внутрь, ответ — изнутри наружу. Разберём идею на чистом Python, не запуская сервер:
def make_mw(name, handler):
def wrapper(request):
print(f"-> {name}: вход")
response = handler(request)
print(f"<- {name}: выход")
return response
return wrapper
def endpoint(request):
print(" [эндпоинт]")
return "ok"
# add_middleware(A); add_middleware(B): B обернёт A
app = make_mw("A", endpoint) # добавлена первой -> ближе к эндпоинту
app = make_mw("B", app) # добавлена второй -> внешний слой
print(app("req"))
Вывод:
-> B: вход -> A: вход [эндпоинт] <- A: выход <- B: выход ok
Запрос «ныряет» через внешний B, затем A, доходит до эндпоинта, и ответ всплывает обратно в обратном порядке. Практический вывод: middleware, который должен видеть ответ последним (например, общий обработчик ошибок или логгер итогового статуса), добавляйте последним, чтобы он стал самым внешним слоем.
Готовые middleware: CORS и GZip
Самописать всё не нужно — частые задачи закрывают встроенные классы, подключаемые через app.add_middleware.
CORS — доступ из браузера с другого домена
Без CORS браузер заблокирует запрос с app.example.com к вашему API на api.example.com. CORSMiddleware добавляет нужные заголовки:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # конкретные домены
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
allow_origins=["*"] разрешает любой источник — приемлемо для публичного открытого API, но несовместимо с allow_credentials=True (нельзя одновременно «любой домен» и «слать куки»). Для продакшена перечисляйте домены явно.
GZip — сжатие ответов
GZipMiddleware сжимает крупные ответы, экономя трафик. minimum_size отсекает мелочь, которую сжимать невыгодно:
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000) # сжимать ответы > 1 КБ
Как это работает под капотом
FastAPI построен на ASGI, и middleware — это ASGI-приложения, обёрнутые одно в другое во время старта. Декоратор @app.middleware("http") и метод add_middleware добавляют слой в стек, который собирается в единую вложенную «луковицу» при первом запросе (а на современном Starlette — при событии lifespan-старта). Поэтому стек middleware фиксируется на старте: добавлять middleware после запуска приложения нельзя. Ключевое архитектурное отличие от зависимостей: middleware оборачивает весь процесс, включая маршрутизацию, и не знает, какой именно эндпоинт сработает и какие у него параметры — он работает на «сыром» уровне запрос/ответ. Depends же выполняется уже после выбора маршрута, видит распарсенные параметры и может вернуть значение прямо в обработчик. Грубое правило: сквозное и не привязанное к конкретному пути (CORS, сжатие, лог, тайминг) — middleware; «дай мне сессию БД / текущего пользователя для этого эндпоинта» — зависимость.
Частые ошибки
- Забыли
return responseв своей middleware — клиент не получает ответ, запрос «зависает» или падает. - Перепутали порядок: думают, что первая добавленная middleware — внешняя. Наоборот, последняя добавленная оборачивает остальные.
- Поставили
allow_origins=["*"]вместе сallow_credentials=True— браузер отвергнет такую комбинацию, куки слаться не будут. - Тяжёлую блокирующую работу (запрос к БД, обращение к диску) выполняют синхронно прямо в
async-middleware — это блокирует весь event loop и роняет производительность. - Пытаются достать в middleware параметры пути или тело, разобранное в схему, — на этом уровне их ещё нет; для этого используют зависимости.
Итоги
- Middleware оборачивает обработку: код до
await call_next()работает на входе, после — на выходе;responseнужно вернуть. - Порядок — «луковица»: последняя добавленная middleware становится самым внешним слоем.
- Свою middleware пишут через
@app.middleware("http"), готовые подключают черезapp.add_middleware(Класс, ...). CORSMiddlewareразрешает кросс-доменные запросы;allow_origins=["*"]несовместим сallow_credentials=True.- Middleware — для сквозных задач на «сыром» уровне;
Depends— когда нужны параметры конкретного эндпоинта.