2025, Sep 24 07:16
Почему маршруты FastAPI ломаются на Vercel и как это исправить
Разбираем, почему маршруты FastAPI на Vercel падают из-за завершающего слэша и RedirectSlashes, и даём решение: отключить редиректы и обслуживать оба пути.
Маршруты FastAPI ведут себя по‑разному локально и на Vercel? Если ваши эндпоинты то срабатывают, то падают в зависимости от завершающего слэша, вы, скорее всего, столкнулись с несоответствием в нормализации редиректов. Локально FastAPI снисходительно относится к слэшам; на Vercel же edge/прокси‑слой обрабатывает их иначе и может приводить к циклам, 404 или даже 500. Ниже — практичный способ сделать API стабильным в обоих окружениях.
Минимальный пример, который воспроизводит проблему
Ниже приведён маршрут внутри роутера с префиксом /users. Локально он корректно работает и со слэшем на конце, и без него — благодаря стандартному редиректу FastAPI.
from fastapi import APIRouter, Depends
from ..schemas.user import UserView
from ..auth import ensure_admin
from ..repositories.users import fetch_user_list
users_api = APIRouter(prefix="/users", tags=["users"]) 
@users_api.get("/", response_model=list[UserView])
def fetch_users(_: UserView = Depends(ensure_admin)):
    return fetch_user_list()
В локальной разработке с Uvicorn запрос к /api/users перенаправляется на /api/users/ (307), и маршрут совпадает. На Vercel же вызов /api/users/ может дать 500, а /api/users — привести к 404 или net::ERR_TOO_MANY_REDIRECTS. Если поменять декоратор на пустой путь, например @get(""), картина меняется: на Vercel всё начинает работать, но локально запросы начинают возвращать 404.
Что на самом деле происходит
Это несоответствие в нормализации завершающего слэша. FastAPI (через Starlette) по умолчанию включает RedirectSlashes. Локально это означает, что /api/users автоматически перенаправляется на /api/users/, и далее сопоставляется с маршрутом @get("/"). На Vercel же некий компонент в edge/прокси‑слое нормализует или удаляет завершающий слэш до того, как запрос попадёт в ваше приложение, — и автоматический редирект либо отбрасывается, либо зацикливается, либо обрабатывается иначе. В итоге там надёжно работает только маршрут, объявленный на "", тогда как локально ожидается редирект на "/".
Здесь задействованы прокси: в разработке Next.js проксирует /api/:path* в FastAPI, а в продакшне Vercel направляет /api/(.*) → /api/index.py. Эти уровни влияют на то, как сохраняется или перенаправляется слэш до того момента, как запрос обработает ваше приложение FastAPI.
Исправление, которое работает в любых окружениях
Надёжный подход — перестать полагаться на RedirectSlashes и явно обслуживать обе версии пути. Сначала отключите автоматический редирект в приложении FastAPI. Затем повесьте два обработчика на одну и ту же функцию: для пустого пути и для пути со слэшем на конце. Так и /api/users, и /api/users/ попадут прямо в ваш код без каких‑либо редиректов.
# main/index.py
from fastapi import FastAPI
core_app = FastAPI(title=config.APP_TITLE, lifespan=app_lifespan, redirect_slashes=False)
# routes/users.py
from fastapi import APIRouter, Depends
from ..schemas.user import UserView
from ..auth import ensure_admin
from ..repositories.users import fetch_user_list
users_api = APIRouter(prefix="/users", tags=["users"]) 
@users_api.get("", include_in_schema=False, response_model=list[UserView])  # /api/users
@users_api.get("/", response_model=list[UserView])                          # /api/users/
def fetch_users(_: UserView = Depends(ensure_admin)):
    return fetch_user_list()
Такой приём с двумя декораторами устраняет неоднозначность и исключает редиректы, зависящие от слэша, которые Vercel может обрабатывать по‑разному. Флаг include_in_schema=False сохраняет чистоту OpenAPI‑интерфейса: в документации остаётся один канонический путь, но запросы принимаются по обоим.
Почему это важно
API за прокси и CDN нередко сталкиваются с тонкостями в нормализации путей. Полагаться на редиректы на уровне фреймворка ненадёжно, когда edge‑сеть сначала упрощает или переписывает путь запроса. Отказ от редиректов и явная обработка обеих форм URL гарантируют доступность эндпоинтов локально, в серверлесс‑средах и сквозь цепочку прокси. Это также предотвращает неприятные сбои вроде 404 и циклических перенаправлений, которые сложно воспроизвести.
Итоги
Если эндпоинт локально работает, а на Vercel падает из‑за завершающего слэша, отключите RedirectSlashes и свяжите варианты со слэшем и без него с одним обработчиком. В схеме оставьте документированным только один путь, второй — скрывайте. Это небольшое изменение снимает неопределённость с редиректами между окружениями и стабилизирует деплой FastAPI + Next.js на Vercel без условной логики под разные среды.
Статья основана на вопросе на StackOverflow от AmericaDoodles и ответе автора AmericaDoodles.