Настройка CORS в FastAPI: пошаговая инструкция с нуля
Фронтенд на localhost:3000 отправляет запрос к API на localhost:8000 — и браузер блокирует ответ. Разбираемся, почему браузер это делает, и настраиваем CORSMiddleware в FastAPI так, чтобы всё работало и в разработке, и в проде.

Что происходит и почему браузер блокирует запрос
Вы подняли FastAPI-приложение на localhost:8000, открыли фронтенд на localhost:3000, отправили fetch-запрос — и в консоли браузера увидели:
Access to fetch at 'http://localhost:8000/api/items' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Сервер запрос обработал, данные вернул — но браузер отказался отдавать ответ клиентскому JavaScript. Это механизм CORS — Cross-Origin Resource Sharing. Он не защищает сервер, он защищает пользователя: без явного разрешения от сервера браузер запрещает одному источнику читать ответы другого.
«Источник» (origin) — это тройка: схема + хост + порт. http://localhost:3000 и http://localhost:8000 — два разных источника, даже если физически это одна машина. Достаточно разницы в одном компоненте.
Что нужно до начала
- Python 3.9+ и установленный FastAPI с Uvicorn. Если ещё нет:
pip install "fastapi[standard]"(FastAPI 0.100+). - Любой фронтенд или хотя бы
curl/ браузерная консоль для проверки. - Понимание, что такое HTTP-заголовки. Знать CORS заранее не нужно — разберём по ходу.
Шаг 1 — минимальное приложение без CORS
Создайте файл main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/items")
async def get_items():
return [{"id": 1, "name": "Тестовый элемент"}]
Запустите:
uvicorn main:app --reload --port 8000
Проверка: откройте http://localhost:8000/api/items в браузере — увидите JSON. Через curl тоже всё работает. Но fetch из скрипта на другом порту или домене — заблокирован.
Шаг 2 — подключаем CORSMiddleware
FastAPI использует CORSMiddleware из пакета Starlette, который идёт в зависимостях. Отдельно ставить ничего не надо.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Список источников, которым разрешён доступ
allowed_origins = [
"http://localhost:3000",
"http://localhost:5173", # Vite dev-сервер по умолчанию
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True, # разрешить отправку cookies
allow_methods=["*"], # GET, POST, PUT, DELETE и т.д.
allow_headers=["*"], # любые заголовки, включая Authorization
)
@app.get("/api/items")
async def get_items():
return [{"id": 1, "name": "Тестовый элемент"}]
Проверка: перезапустите Uvicorn, повторите fetch из фронтенда — ответ приходит, ошибка в консоли исчезла.
Что делает каждый параметр
| Параметр | Что задаёт | Значение по умолчанию |
|---|---|---|
allow_origins | Список допустимых источников | [] — всё запрещено |
allow_methods | HTTP-методы, разрешённые для кросс-доменных запросов | ["GET"] |
allow_headers | Заголовки запроса, которые клиент может отправлять | [] |
allow_credentials | Разрешить передачу cookies и заголовка Authorization | False |
expose_headers | Заголовки ответа, доступные JavaScript | [] |
max_age | Сколько секунд браузер кеширует результат preflight-запроса | 600 |
Шаг 3 — проверяем preflight-запрос
Когда фронтенд отправляет не «простой» запрос (например, POST с заголовком Content-Type: application/json), браузер сначала шлёт предварительный OPTIONS-запрос — preflight. Сервер должен ответить нужными CORS-заголовками, иначе основной запрос не уйдёт.
Проверьте это через curl:
curl -X OPTIONS http://localhost:8000/api/items \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-i
В ответе должны быть заголовки:
access-control-allow-origin: http://localhost:3000
access-control-allow-methods: POST
access-control-allow-headers: Content-Type
access-control-allow-credentials: true
Если вместо этого пришёл 200 OK без CORS-заголовков — middleware не подключился. Перечитайте шаг 2: add_middleware вызывается до определения маршрутов или после — не важно, но вызываться должен ровно один раз.
Шаг 4 — конфигурация для продакшена
В разработке allow_origins=["*"] допустим. В продакшене — нет, и вот почему: подстановочный знак * несовместим с allow_credentials=True. Браузер откажется принимать ответ, если сервер одновременно выставляет Access-Control-Allow-Origin: * и Access-Control-Allow-Credentials: true. Это не ограничение FastAPI — это спецификация Fetch.
Звёздочка в allow_origins означает «любой сайт в интернете может читать ответы вашего API от имени залогиненного пользователя». Если у вас есть авторизация — это дыра, а не настройка.
Надёжный подход — брать список допустимых источников из переменной окружения:
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# В .env: CORS_ORIGINS=https://example.com,https://admin.example.com
raw_origins = os.getenv("CORS_ORIGINS", "")
allowed_origins = [origin.strip() for origin in raw_origins.split(",") if origin.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=3600, # кешировать preflight на час
)
Проверка: задайте CORS_ORIGINS при запуске и убедитесь, что запрос с неуказанного домена блокируется.
Если не получилось: типичные ошибки
- Ошибка: «No 'Access-Control-Allow-Origin' header» даже после добавления middleware. Причина — опечатка в origin: лишний слэш в конце (
http://localhost:3000/— не то же, чтоhttp://localhost:3000). В спискеallow_originsпишите без завершающего/. - Ошибка: preflight возвращает 405 Method Not Allowed. Вероятно, перед FastAPI стоит обратный прокси (Nginx, Traefik), который перехватывает OPTIONS. Убедитесь, что прокси пропускает OPTIONS до приложения, или настройте CORS на уровне прокси, а не в FastAPI.
- Cookies не приходят. Проверьте три вещи разом:
allow_credentials=Trueна сервере,credentials: "include"вfetchна фронте, и origin не*. Все три условия обязательны. - Работает в Chrome, не работает в Firefox. Firefox строже кеширует preflight. Очистите кеш или добавьте
max_age=0на время отладки. - Ошибка на POST, но не на GET. GET без кастомных заголовков — «простой» запрос, preflight не нужен. POST с
application/json— уже не простой. Убедитесь, чтоallow_headersвключаетContent-Type.
Что CORS не защищает
CORS — не замена аутентификации. Он работает только в браузере. curl, серверный код, мобильное приложение отправят запрос без всяких preflight и получат ответ независимо от ваших CORS-настроек. Если API должен быть закрыт — закрывайте его токенами, API-ключами, проверкой IP, rate-limiting. CORS — про политику браузера, а не про безопасность сервера.
Что делать дальше
После настройки CORS в FastAPI стоит проверить поведение за обратным прокси: часто CORS конфигурируют и в Nginx, и в приложении — получается конфликт, когда заголовок Access-Control-Allow-Origin приходит дважды, и браузер это отвергает. Выберите одно место. Если API обслуживается Nginx — проще вынести CORS в конфиг Nginx, а CORSMiddleware из FastAPI убрать. Если API отдаётся напрямую или через минимальный прокси — оставьте настройку в приложении.
Если вы используете WebSocket-эндпоинты в FastAPI — учтите, что CORS к WebSocket не применяется: браузер не шлёт preflight для ws://. Защита WebSocket — отдельная тема.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: FastAPI — официальная документация: CORS (Cross-Origin Resource Sharing), MDN Web Docs — Cross-Origin Resource Sharing (CORS)
Частые вопросы
Можно ли поставить allow_origins=["*"] и allow_credentials=True одновременно?
CORS блокирует запросы с сервера или из curl?
Почему GET работает, а POST с JSON — нет?
Что будет, если настроить CORS и в Nginx, и в FastAPI?
Как разрешить CORS только для определённых эндпоинтов, а не для всего приложения?
У меня другая версия FastAPI — инструкция сработает?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.