Перейти к содержанию

Ограничения и внутреннее устройство

Честный список того, что осталось за рамками, и немного о том, как всё устроено внутри.

Версии FastAPI

Ограничение зависимости — fastapi >=0.95.0,!=0.137.0,!=0.137.1.

Версия FastAPI Статус Как обходятся роуты
0.950.136 Поддерживается Плоский список router.routes
0.137.0, 0.137.1 Исключены Дерево роутов уже есть, публичного обходчика ещё нет
0.137.2 и новее Поддерживается Публичный iter_route_contexts

В 0.137.0 произошёл рефакторинг роутинга: include_router перестал копировать роуты, а router.routes стал деревом. Поддерживаемый способ обойти это дерево — iter_route_contexts — появился только в 0.137.2, поэтому промежуток из двух релизов исключён на уровне зависимости.

Нужный режим выбирается автоматически: обходчик ищется через getattr при импорте, при его отсутствии используется плоский обход.

Что проверяет CI

Тесты гоняются на минимальной поддерживаемой (0.95), последней до рефакторинга (0.136) и новейшей версии FastAPI — на каждом из питонов 3.10–3.14.

Версионирование только по пути

Библиотека версионирует смонтированные субприложения, то есть версия всегда выражена префиксом пути (/v1, /api/public/v2). Версионирование по заголовку (Accept: application/vnd.api+json; version=2), по query-параметру или по поддомену не поддерживается: версия определяется маршрутизацией ASGI-mount'ов ещё до того, как в дело вступает эта библиотека.

Пометка обязательна

Эндпоинт без Depends(versioning()) невидим для версионирования. Это осознанное решение — неявное наследование всех подряд роутов слишком легко приводит к тому, что в новую версию просачивается то, что там не планировалось. Обратная сторона: забытая пометка выглядит как «эндпоинт почему-то не наследуется».

Момент сборки

Наследование строится один раз — при первом ASGI-событии:

  • под реальным сервером это событие запуска lifespan;
  • в тестах через ASGI-транспорт (lifespan не выполняется) и для middleware на смонтированном приложении — первый запрос.

До этого момента ничего не версионировано: копирование роутов происходит в рантайме, а не на этапе импорта или декорирования. Всё, что добавлено после, требует явного rebuild_versioning.

Типы api_version

Только int, причём bool отвергается отдельно, несмотря на то что он подкласс int. Значения "1", 1.0, True приводят к тому, что субприложение игнорируется — с UserWarning, чтобы это не выглядело как молчаливая пропажа версии. Субприложение вовсе без api_version игнорируется молча: это штатный способ смонтировать рядом что-то неверсионированное.

Затенение и совпадающие пути

Наследование в версию пропускается, если там уже есть роут с тем же путём и теми же методами. Отсюда два следствия:

  • GET /items в v2 не затеняет унаследованный POST /items — это разные наборы методов;
  • HTTP-роут и WebSocket-роут на одном пути не конфликтуют вовсе: затенение учитывает вид роута.

WebSocket на fastapi 0.95

В 0.95 у websocket() ещё нет параметра dependencies, поэтому пометить WS-роут можно только зависимостью в сигнатуре эндпоинта:

@app_v1.websocket("/ws")
async def ws(
    websocket: WebSocket,
    version: Annotated[VersionInfo, Depends(versioning())],
) -> None: ...

На более новых FastAPI работают оба способа. Рецепт с табами — в Рецептах.

Копии роутов, а не общий объект

Каждая наследующая версия получает собственную копию роута. Это делает версии независимыми (dependency_overrides резолвятся приложением той версии, которая обслуживает запрос), но означает, что модификация объекта роута в одной версии не отразится на других — менять нужно исходный роут и вызывать rebuild_versioning.

Роуты, пришедшие из include_router, не копируются, а пересобираются заново из их эффективного контекста: иначе потерялись бы include-префикс и зависимости, добавленные при включении.