Веб-разработка

CORS простыми словами: почему запрос работает в Postman, но падает в браузере

Классическая ситуация: фронтенд стучится на API, в Postman или curl всё отвечает как надо, а в браузере в консоли появляется красная строка про CORS — и запрос просто не выполняется. Разработчик, который впервые с этим сталкивается, обычно решает проблему методом копипасты первого найденного заголовка, который «вроде помогает». Работает — и ладно. Но CORS устроен вполне логично, и если понять, из-за чего он вообще существует, отладка таких ошибок перестаёт быть гаданием.

Same-Origin Policy: откуда растёт ограничение

CORS не берётся из ниоткуда — он существует поверх более старого и куда более строгого правила браузеров, которое называется Same-Origin Policy (SOP). Суть в следующем: скрипт, загруженный с одного источника (origin — это связка протокол + домен + порт), по умолчанию не может свободно читать ответы от запросов к другому origin. Если хотя бы один из трёх компонентов отличается — другой порт, другой домен, другой протокол — origin считается другим.

Это ограничение придумали не для того, чтобы мешать разработчикам, а чтобы защитить пользователя. Представьте, что вы открыли вкладку с банковским сайтом, а в соседней вкладке — вредоносная страница. Без SOP скрипт на вредоносной странице мог бы тихо сходить в API банка от имени вашей сессии (используя ваши куки) и прочитать ответ. SOP это блокирует по умолчанию — запрос может уйти, но браузер не отдаст скрипту доступ к результату, если сервер явно не разрешил.

CORS (Cross-Origin Resource Sharing) — это протокол, который позволяет серверу явно сказать браузеру: «этому origin можно читать мой ответ». Ошибка CORS в консоли — это не сбой сети и не проблема сервера в привычном смысле, это браузер сам заблокировал доступ к уже полученному ответу, потому что сервер не подтвердил разрешение.

Simple request и preflight: два разных сценария

Браузер обрабатывает кросс-доменные запросы по-разному в зависимости от того, насколько они «безобидны» с точки зрения спецификации.

Это объясняет частое недоумение: «в логах сервера видно два запроса на один клик». Второй запрос — это и есть preflight, он не доходит до вашего обработчика, если тот не настроен отвечать на OPTIONS.

Почему Access-Control-Allow-Origin: * не всегда решает проблему

Заголовок с звёздочкой — самый частый «быстрый фикс», который находят в интернете. Он действительно разрешает читать ответ любому origin, но у него есть важное ограничение: он не работает вместе с запросами, которые несут учётные данные (куки, HTTP-аутентификацию, TLS-сертификаты клиента) — то есть с запросами, где на клиенте включено credentials: 'include'. Спецификация прямо запрещает браузеру принимать wildcard вместе с Access-Control-Allow-Credentials: true — сервер обязан вернуть конкретный origin, а не звёздочку.

CORS защищает не сервер, а пользователя браузера. Именно поэтому его нельзя «настроить понадёжнее» на клиенте — все решения находятся только на стороне сервера.

Отсюда вытекает и другое частое заблуждение: пытаться «обойти CORS» на фронтенде — расширениями браузера, прокси в коде клиента, отключением проверки в настройках. Формально это может сработать у конкретного разработчика в его браузере, но никак не поможет реальным пользователям продукта, потому что защита работает именно в их браузере, а не в вашем окружении для разработки.

Как настраивать CORS правильно

Практичный подход — вместо того чтобы гуглить конкретную ошибку и вставлять первый попавшийся заголовок, разобраться, что именно нужно вашему случаю:

  1. Определите точный список разрешённых origin. Вместо звёздочки перечислите домены, которым реально нужен доступ — прод, стейджинг, локальная разработка — и возвращайте Access-Control-Allow-Origin динамически, сверяя его со списком на сервере.
  2. Явно перечислите разрешённые методы и заголовки. Access-Control-Allow-Methods и Access-Control-Allow-Headers должны включать только то, что реально используется — например, если фронтенд шлёт Authorization, этот заголовок должен быть в списке.
  3. Включайте credentials только там, где они реально нужны. Если API не работает с куками или сессиями на основе кук, а использует, например, токен в заголовке, `Access-Control-Allow-Credentials` можно не выставлять вовсе — это упрощает остальную настройку.
  4. Не забывайте про кеширование preflight. Заголовок Access-Control-Max-Age позволяет браузеру запомнить результат preflight-запроса на какое-то время и не повторять его перед каждым «настоящим» запросом — это заметно снижает число лишних round-trip'ов.
  5. Проверяйте настройку не только через Postman. Инструменты вроде curl и Postman не применяют политику Same-Origin вообще, поэтому там CORS-ошибок в принципе не бывает — рабочий сценарий нужно проверять именно в браузере, из настоящего фронтенд-приложения.

Итог

CORS — это не досадное препятствие, которое ставится между фронтендом и API, а часть защиты пользователя от чужих скриптов, читающих его данные без разрешения. Ошибка в консоли браузера почти всегда означает одно из двух: сервер не знает про origin, с которого пришёл запрос, либо конфигурация не учитывает нюанс вроде credentials или preflight. Разобравшись один раз в механике simple request и preflight, вы перестанете тратить время на подбор заголовков наугад и начнёте читать ошибку CORS так же спокойно, как обычный 404.

← Все статьи
Я люблю Алину Цой (Билялову)