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).
Проверьте себя
1. Что такое origin для политики CORS?
AТолько доменное имя
BСочетание схемы, хоста и порта
CIP-адрес сервера
DПуть к ресурсу
2. Кто именно блокирует ответ при ошибке CORS?
AСервер API
BБраузер (через Same-Origin Policy)
CОперационная система
DDNS-сервер
3. Каким заголовком сервер разрешает cross-origin доступ?
AContent-Type
BAccess-Control-Allow-Origin
CSet-Cookie
DCache-Control
4. Что такое preflight-запрос?
AПервый запрос при загрузке страницы
BПредварительный OPTIONS-запрос, которым браузер спрашивает разрешение перед сложным cross-origin запросом
CЗапрос к DNS
DПроверка сертификата
Поддержать проект