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: два разных сценария
Браузер обрабатывает кросс-доменные запросы по-разному в зависимости от того, насколько они «безобидны» с точки зрения спецификации.
- Simple request. Запрос считается простым, если это GET, POST или HEAD, без нестандартных заголовков, а Content-Type — один из ограниченного набора (например,
application/x-www-form-urlencoded,multipart/form-dataилиtext/plain). Такой запрос браузер отправляет сразу, а уже потом смотрит на заголовокAccess-Control-Allow-Originв ответе — и либо отдаёт результат скрипту, либо блокирует. - Preflight-запрос. Если запрос не подпадает под «простой» — типичный пример:
Content-Type: application/json, кастомные заголовки вродеAuthorization, методы PUT/DELETE/PATCH — браузер сначала сам отправляет отдельный запрос методом OPTIONS. В нём он спрашивает сервер: «а можно ли вообще так делать этому origin, этим методом, с этими заголовками?». Только если сервер отвечает разрешающими заголовками, браузер отправляет настоящий запрос.
Это объясняет частое недоумение: «в логах сервера видно два запроса на один клик». Второй запрос — это и есть preflight, он не доходит до вашего обработчика, если тот не настроен отвечать на OPTIONS.
Почему Access-Control-Allow-Origin: * не всегда решает проблему
Заголовок с звёздочкой — самый частый «быстрый фикс», который находят в интернете. Он действительно разрешает читать ответ любому origin, но у него есть важное ограничение: он не работает вместе с запросами, которые несут учётные данные (куки, HTTP-аутентификацию, TLS-сертификаты клиента) — то есть с запросами, где на клиенте включено credentials: 'include'. Спецификация прямо запрещает браузеру принимать wildcard вместе с Access-Control-Allow-Credentials: true — сервер обязан вернуть конкретный origin, а не звёздочку.
CORS защищает не сервер, а пользователя браузера. Именно поэтому его нельзя «настроить понадёжнее» на клиенте — все решения находятся только на стороне сервера.
Отсюда вытекает и другое частое заблуждение: пытаться «обойти CORS» на фронтенде — расширениями браузера, прокси в коде клиента, отключением проверки в настройках. Формально это может сработать у конкретного разработчика в его браузере, но никак не поможет реальным пользователям продукта, потому что защита работает именно в их браузере, а не в вашем окружении для разработки.
Как настраивать CORS правильно
Практичный подход — вместо того чтобы гуглить конкретную ошибку и вставлять первый попавшийся заголовок, разобраться, что именно нужно вашему случаю:
- Определите точный список разрешённых origin. Вместо звёздочки перечислите домены, которым реально нужен доступ — прод, стейджинг, локальная разработка — и возвращайте
Access-Control-Allow-Originдинамически, сверяя его со списком на сервере. - Явно перечислите разрешённые методы и заголовки.
Access-Control-Allow-MethodsиAccess-Control-Allow-Headersдолжны включать только то, что реально используется — например, если фронтенд шлётAuthorization, этот заголовок должен быть в списке. - Включайте credentials только там, где они реально нужны. Если API не работает с куками или сессиями на основе кук, а использует, например, токен в заголовке, `Access-Control-Allow-Credentials` можно не выставлять вовсе — это упрощает остальную настройку.
- Не забывайте про кеширование preflight. Заголовок
Access-Control-Max-Ageпозволяет браузеру запомнить результат preflight-запроса на какое-то время и не повторять его перед каждым «настоящим» запросом — это заметно снижает число лишних round-trip'ов. - Проверяйте настройку не только через Postman. Инструменты вроде curl и Postman не применяют политику Same-Origin вообще, поэтому там CORS-ошибок в принципе не бывает — рабочий сценарий нужно проверять именно в браузере, из настоящего фронтенд-приложения.
Итог
CORS — это не досадное препятствие, которое ставится между фронтендом и API, а часть защиты пользователя от чужих скриптов, читающих его данные без разрешения. Ошибка в консоли браузера почти всегда означает одно из двух: сервер не знает про origin, с которого пришёл запрос, либо конфигурация не учитывает нюанс вроде credentials или preflight. Разобравшись один раз в механике simple request и preflight, вы перестанете тратить время на подбор заголовков наугад и начнёте читать ошибку CORS так же спокойно, как обычный 404.
← Все статьи