Маршрут FastAPI исчез из Swagger, но остался доступен

После добавления include_in_schema=False служебная диагностика исчезла из Swagger UI. Разработчик считает задачу ограничения доступа выполненной. Однако клиент по прежнему может обратиться к известному URL: параметр управляет документацией, а не маршрутизацией. Особенно легко пропустить это на endpoint, который не имеет ссылки в интерфейсе.

Два независимых свойства маршрута

OpenAPI сообщает клиентам, какие операции документированы. Таблица маршрутов приложения определяет, какие запросы исполняются. Скрытая операция может оставаться полезной для внутренней автоматики, но ей всё равно нужна проверка полномочий. Настройка docs_url=None также убирает страницу документации, не отключая остальные URL приложения.

Сделаем диагностический endpoint невидимым в публичной схеме и доступным только по учебному служебному credential. В реальном приложении та же точка Depends должна возвращать подтверждённого оператора из вашей системы входа. Ответ диагностики ограничим идентификатором ревизии, чтобы не выдавать конфигурацию, DSN и секреты даже разрешённому читателю без необходимости.

Проверим и схему, и реальный запрос

Понадобятся fastapi и httpx. В тесте сначала подтверждается отсутствие URL в /openapi.json. Затем выполняется запрос прямо к этому URL без credential. Наконец, корректный заголовок доказывает, что маршрутизация продолжает работать. Один тест схемы обнаружил бы только косметическое изменение документации.

from typing import Annotated
from fastapi import FastAPI, Depends, Header, HTTPException
from fastapi.testclient import TestClient

app = FastAPI()

def operator(x_ops_key: Annotated[str | None, Header()] = None):
    if x_ops_key != "demo-ops":
        raise HTTPException(403, "operator access required")

@app.get("/status")
def public_status():
    return {"ready": True}

@app.get("/internal/revision", include_in_schema=False,
         dependencies=[Depends(operator)])
def revision():
    return {"revision": "demo-17"}

with TestClient(app) as client:
    paths = client.get("/openapi.json").json()["paths"]
    assert "/internal/revision" not in paths
    assert "/status" in paths
    denied = client.get("/internal/revision")
    assert denied.status_code == 403
    assert client.get("/internal/revision",
                      headers={"X-Ops-Key": "wrong"}).status_code == 403
    allowed = client.get("/internal/revision", headers={"X-Ops-Key": "demo-ops"})
    assert allowed.status_code == 200
    assert allowed.json() == {"revision": "demo-17"}
    print({"documented": sorted(paths), "denied": denied.status_code,
           "allowed": allowed.json()})

Где устанавливать границу доступа

Если служебные операции вынесены в APIRouter, общую зависимость удобно задать роутеру. Тогда новый маршрут получает проверку вместе с подключением. Проверьте итоговое приложение после include_router: тест отдельного роутера способен пропустить ошибку сборки. Для особо чувствительных операций дополнительно разделяют сетевую доступность, но наличие внутренней сети не заменяет прикладную проверку.

Часть диагностических URL должна быть доступна балансировщику без пользовательского входа. В таком случае заранее определите минимальный ответ. Простому признаку готовности обычно не нужны версия Python, содержимое переменных окружения или подробности исключения подключения. Не превращайте удобный endpoint здоровья в универсальную страницу состояния всего окружения.

Если действительно требуется отключить endpoint, уберите его регистрацию или включайте роутер по конфигурации при сборке приложения. Для такого режима ожидаемый результат прямого запроса — 404. Это другое требование, и его следует проверять отдельно. Переключение видимости схемы никогда не стоит использовать вместо него.

После изменения просмотрите также сгенерированные SDK. Скрытая операция не попадёт в новый клиент, хотя старый клиент или ручной HTTP-запрос сможет её вызвать. Если речь о выводе устаревшего публичного метода, нужен процесс совместимости и удаления. В статье об operationId разобрана другая часть этого договора: стабильное имя операции в схеме.

Регрессия при добавлении нового маршрута

Подготовьте тест, который перечисляет служебные пути итогового приложения и проверяет отказ без полномочий. Он особенно полезен при разделении файлов: новый endpoint может оказаться зарегистрирован напрямую на app, минуя защищённый роутер. Проверка наличия Depends в исходнике одного модуля такой ошибки не обнаружит, а прямой HTTP-запрос покажет доступ сразу.

Не объединяйте все служебные действия одной ролью только ради удобства. Чтение ревизии и запуск пересчёта имеют разную цену ошибки. После общей аутентификации добавьте разрешение конкретного действия. Для изменяющей команды сохраните также проверку метода: отсутствие операции в Swagger не мешает выполнить POST вручную, если маршрут действительно зарегистрирован и не требует права.

Продолжить на практике

Попробуйте разделить публичный и служебный роутеры и проверить оба через TestClient. Бесплатный курс FastAPI даёт практику маршрутов, зависимостей и авторизации, которую удобно использовать для такой сборки приложения. Откройте бесплатную программу FastAPI.

Продолжить на практике