FastAPI возвращает 422 на /users/me: как выбрать порядок маршрутов

В приложении уже работает GET /users/17. Вы добавляете GET /users/me для собственного профиля, открываете его и получаете 422: сервер пытается прочитать слово me как целое число. Профиль существует, авторизация здесь ещё ни при чём. Запрос выбрал другой маршрут раньше, чем дошёл до вашего обработчика.

Как FastAPI выбирает обработчик

Маршруты проверяются в порядке регистрации. Шаблон /users/{user_id} подходит строке /users/me на этапе сопоставления пути. Аннотация user_id: int применяется после выбора маршрута: она валидирует значение, но не заставляет роутер искать следующий обработчик при ошибке. Поэтому изменение типа на int само по себе конфликт не устраняет.

Литеральный путь /users/me поставьте перед общим шаблоном. Это правило относится и к другим служебным адресам: /products/search, /orders/export, /files/latest. Сначала сопоставляются известные служебные сегменты, затем произвольный идентификатор. Названия методов HTTP тоже участвуют в выборе: GET и POST на одном пути могут обслуживать разные операции.

Сначала воспроизведите выбор маршрута

Пример создаёт два приложения с одними обработчиками. Меняется только порядок их подключения. Так видно, что ответ 422 возникает до бизнес-логики профиля. Для запуска нужны FastAPI и HTTPX; сохраните весь блок в файл и выполните его Python. TestClient отправляет локальные HTTP-запросы без отдельного Uvicorn.

Пример рассчитан на Python 3.11+ с FastAPI, Pydantic v2 и HTTPX. Установите пакеты fastapi и httpx в отдельное окружение, сохраните блок в файл и запустите его Python.

from fastapi import FastAPI
from fastapi.testclient import TestClient

def make_app(fixed):
    app = FastAPI()
    def own():
        return {"kind": "own"}
    def by_id(user_id: int):
        return {"kind": "by_id", "id": user_id}
    routes = [("/users/me", own), ("/users/{user_id}", by_id)]
    for path, endpoint in routes if fixed else reversed(routes):
        app.add_api_route(path, endpoint, methods=["GET"])
    return app

with TestClient(make_app(False)) as client:
    wrong = client.get("/users/me")
    assert wrong.status_code == 422
    assert wrong.json()["detail"][0]["loc"] == ["path", "user_id"]
with TestClient(make_app(True)) as client:
    assert client.get("/users/me").json() == {"kind": "own"}
    assert client.get("/users/17").json() == {"kind": "by_id", "id": 17}
    assert client.get("/users/no-number").status_code == 422
print("static route, numeric route and invalid id checked")

Почему важно проверить соседние адреса

После исправления /users/me недостаточно проверить только его. Числовой идентификатор должен по-прежнему приводить в by_id, а некорректный числовой путь — возвращать 422. Последняя проверка подтверждает, что вы исправили приоритет, сохранив типизацию. Если вместо неё сделать user_id строкой и разбирать вручную, ошибка станет менее заметной, но контракт идентификатора расплывётся.

Для APIRouter действует тот же принцип. Посмотрите порядок include_router и порядок операций внутри каждого роутера. Два подходящих пути в разных файлах не становятся независимыми: в итоговом приложении всё равно существует последовательность маршрутов. В небольшом проекте полезно временно распечатать route.path и route.methods для app.routes и сопоставить их с адресом, который дал неожиданный ответ.

Как оставить понятную структуру проекта

Если список служебных имён растёт, выделите их в явную группу, например /users/me и /admin/users/search, либо выберите адрес ресурса, который исключает неоднозначность. Не меняйте публичные URL только ради порядка без проверки клиентов: сохранение адресов с правильной регистрацией обычно дешевле для существующего API.

В регрессионном тесте фиксируйте пару «служебный путь — путь с идентификатором». Такой тест легко читать после переноса файлов. Ошибка 422 с loc path указывает на разбор сегмента, тогда как loc query или body отправляет вас к параметрам запроса или модели. Это позволяет начать диагностику с нужной границы и не менять схему JSON для ошибки маршрутизации.

Следующая практика

В открытом уроке о path-параметрах вы свяжете часть URL с аргументом функции и проверите, как FastAPI обрабатывает тип идентификатора. Это подходящий следующий шаг после исправления порядка маршрутов. Откройте бесплатный урок и выполните практику.

В бесплатном курсе FastAPI вы затем соедините маршруты, базу, регистрацию и тесты в одном приложении. Когда понадобится воспроизводимо запускать этот backend на другом компьютере, откройте программу Docker для разработчика: в ней есть образы, контейнеры и Compose для приложения с базой.

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