CORS: почему браузер блокирует запросы
Разбираемся с самой частой загадкой фронтендера: почему запрос блокируется ошибкой CORS и как это починить.
CORS (Cross-Origin Resource Sharing) — механизм, которым сервер разрешает браузеру обращаться к нему со страниц другого origin. По умолчанию браузер такие запросы ограничивает.
Что такое origin
Origin (источник) — это тройка: схема + хост + порт. Два URL имеют один origin, только если совпадают все три. Проверим на Python:
from urllib.parse import urlparse
def origin(url):
p = urlparse(url)
port = p.port or (443 if p.scheme == 'https' else 80)
return (p.scheme, p.hostname, port)
base = 'https://app.example.com/page'
print('База:', origin(base))
print()
for url in ['https://app.example.com/other',
'https://api.example.com/data',
'http://app.example.com/page',
'https://app.example.com:8443/x']:
same = origin(url) == origin(base)
print(f'{url:38} same-origin={same}')
Вывод:
База: ('https', 'app.example.com', 443)
https://app.example.com/other same-origin=True
https://api.example.com/data same-origin=False
http://app.example.com/page same-origin=False
https://app.example.com:8443/x same-origin=False
Достаточно отличия в схеме, хосте или порту — и это уже другой origin. Именно поэтому фронтенд на localhost:3000 и API на localhost:8000 — разные origin.
Политика одного источника (Same-Origin Policy)
В основе лежит правило безопасности браузеров: скрипт со страницы может свободно читать ответы только с того же origin. Зачем? Чтобы вредоносный сайт evil.com, открытый в соседней вкладке, не мог через ваш браузер дёргать bank.com и читать ваши данные (пользуясь тем, что вы залогинены). Это фундаментальная защита.
Откуда берётся ошибка CORS
Когда ваш JS на app.example.com делает fetch к api.other.com, браузер видит cross-origin запрос. Он отправляет его, но не отдаёт ответ вашему коду, пока сервер явно не разрешит — через заголовок Access-Control-Allow-Origin. Если разрешения нет, в консоли появляется знаменитое:
Access to fetch at 'https://api.other.com/data' from origin
'https://app.example.com' has been blocked by CORS policy
Важный нюанс: блокирует именно браузер, а не сервер. Запрос мог дойти и даже выполниться — но браузер прячет ответ от скрипта в целях безопасности. Поэтому из Postman или curl тот же запрос работает: там нет Same-Origin Policy.
Как сервер разрешает доступ
Сервер добавляет в ответ CORS-заголовки:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Только увидев Access-Control-Allow-Origin со своим origin (или *), браузер отдаёт ответ коду.
Preflight-запрос
Для «непростых» запросов (методы PUT/DELETE, кастомные заголовки) браузер сначала шлёт preflight — предварительный запрос методом OPTIONS: «а можно мне вообще сделать PUT с заголовком Authorization?». Сервер отвечает разрешениями, и только тогда летит настоящий запрос. Поэтому в Network вы иногда видите два запроса вместо одного.
Как чинить CORS
- Правильно: настроить CORS-заголовки на сервере API, разрешив нужный origin.
- В разработке: dev-прокси (фронтенд-сервер проксирует запросы к API, и для браузера это «тот же origin»).
- Не делайте: бездумно ставить
Access-Control-Allow-Origin: *на API с приватными данными — это снимает защиту.
Итог
- Origin = схема + хост + порт; отличие любого — это cross-origin.
- Same-Origin Policy не даёт чужому сайту читать ответы вашего API через браузер.
- Ошибку CORS вызывает браузер, скрывая ответ без разрешения сервера.
- Сервер разрешает доступ заголовком
Access-Control-Allow-Origin; для сложных запросов идёт preflight (OPTIONS).