Как сделать редирект на страницу авторизации при ошибке 401 в FastAPI
Что проверяют
Понимаете ли вы, где в FastAPI перехватываются ошибки, и различаете ли браузерные страницы и API. Уточнение почти всегда одно: что получит фронтенд, который ходит в тот же сервис через fetch.
Как отвечать
Правильное место это обработчик исключений, а не проверка каждого ответа:
from fastapi.exception_handlers import http_exception_handler
from fastapi.responses import RedirectResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
@app.exception_handler(StarletteHTTPException)
async def auth_redirect(request, exc):
if exc.status_code == 401 and "text/html" in request.headers.get("accept", ""):
return RedirectResponse(f"/login?next={request.url.path}", status_code=303)
return await http_exception_handler(request, exc)Что стоит проговорить:
- регистрация на класс Starlette: так ловятся и ваши
HTTPException, и те, что поднимает сам фреймворк; - разделение клиентов: HTML-страницам редирект, API остаётся 401 с JSON и заголовком
WWW-Authenticate. Признак: заголовокAccept, префикс/apiили отдельный роутер; - код редиректа:
RedirectResponseпо умолчанию отдаёт 307, он сохраняет метод. После POST нужен 303, чтобы браузер пошёл на/loginчерез GET; - своё исключение: зависимость
require_userподнимаетNotAuthenticated, а отдельный обработчик превращает его в редирект. Поведение задаётся явно, а не по коду статуса; - 401 и 403: на вход ведёт только 401, «не аутентифицирован». 403 означает «вошёл, но нельзя», редирект на логин там только запутает.
Middleware, которое смотрит на response.status_code, тоже работает, но бьёт по всем ответам, включая API.
Где спотыкаются
Редиректят и API-запросы. fetch по умолчанию молча идёт за редиректом, получает HTML страницы входа со статусом 200, и фронтенд падает на разборе JSON вместо понятного 401.
Кладут next в редирект без проверки. Параметр вида next=https://evil.example превращает вход в открытый редирект, разрешать стоит только относительные пути своего сайта.
Защищают саму страницу /login или её статику той же зависимостью. Получается бесконечный цикл редиректов.