Blocked by CORS policy: как исправить ошибку в JavaScript

опубликовано
читать
≈ 7 мин
автор
среда и версия
Fetch API и CORS в современных браузерах
содержание Решать задачи Современный JavaScript

Сообщение blocked by CORS policy означает, что браузер не разрешил JavaScript прочитать ответ другого origin. Это не обязательно значит, что запрос не дошёл до сервера: простой запрос браузер отправляет сразу, а ответ скрывает от кода, если проверка CORS не пройдена. Запрос с preflight останавливается раньше фактического запроса, когда сервер не разрешил заявленные метод или заголовки.

Исправление обычно находится на сервере или промежуточном прокси. Сервер должен проверить Origin по списку разрешённых источников и вернуть согласованный набор CORS-заголовков. Клиентский try…catch может обработать отказ, но не может выдать странице доступ к заблокированному ответу.

Что браузер считает другим origin

Origin состоит из схемы, хоста и порта. Все три части должны совпасть:

СтраницаAPIРезультат
https://app.example/profilehttps://app.example/api/userОдин origin: путь не участвует в сравнении
https://app.examplehttps://api.exampleРазные хосты
https://app.examplehttp://app.exampleРазные схемы
http://localhost:5173http://localhost:3000Разные порты

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

Браузер сам добавляет заголовок запроса Origin, например:

Origin: https://app.example

JavaScript не должен выставлять его вручную. Сервер сравнивает значение с точным списком разрешённых origin и для совпавшего источника возвращает:

Access-Control-Allow-Origin: https://app.example
Vary: Origin

Этот фрагмент корректен только если https://app.example уже прошёл серверную проверку. Нельзя безусловно копировать в ответ любое присланное значение Origin: тогда список разрешений фактически исчезнет. Access-Control-Allow-Origin принимает один origin, а не список через запятую.

Vary: Origin нужен, когда ответный Access-Control-Allow-Origin зависит от запроса. Он сообщает кешу, что ответы для разных значений Origin нельзя считать одной и той же версией. Если прокси уже формирует Vary по другим полям, Origin добавляют к существующему списку, а не затирают его.

Простой запрос и запрос с preflight

Термин «простой запрос» обозначает узкий набор CORS-запросов, которые браузер отправляет без предварительного OPTIONS. Метод должен быть GET, HEAD или POST; разрешены только заголовки из списка CORS-safelisted, а для явно заданного Content-Type — только application/x-www-form-urlencoded, multipart/form-data или text/plain с ограничениями Fetch Standard.

Например, GET без нестандартных заголовков обычно уходит сразу. Сервер всё равно должен вернуть подходящий Access-Control-Allow-Origin, иначе браузер получит ответ по сети, но не передаст его JavaScript. Поэтому CORS нельзя считать защитой от изменяющих запросов: простой POST способен выполнить действие на сервере до того, как браузер скроет ответ.

Preflight требуется, когда запрос выходит за эти рамки. Типичные причины — PUT, PATCH или DELETE, заголовок Authorization, собственный заголовок наподобие X-Request-ID либо Content-Type: application/json. Сначала браузер отправляет OPTIONS с описанием будущего запроса:

OPTIONS /orders HTTP/1.1
Origin: https://app.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type

По Fetch Standard preflight не передаёт учётные данные. Клиентские TLS-сертификаты — известное браузерное отклонение от этого правила, поэтому на них нельзя строить переносимую логику. Сервер должен обработать OPTIONS до проверки пользовательской сессии и решить, разрешены ли сочетание origin, путь, метод и заголовки. Успешный минимальный ответ может выглядеть так:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Vary: Origin

Этот пример подходит только для маршрута, который разрешает запросы с учётными данными от https://app.example. Перечисляйте лишь реально поддерживаемые методы и заголовки. После успешного preflight браузер отправит фактический POST; его ответ тоже обязан содержать Access-Control-Allow-Origin, а при учётных данных — Access-Control-Allow-Credentials: true.

JavaScript / 01

Preflight проверяет разрешение до основного POST

Preflight проверяет разрешение до основного POSTБраузер / app.example API / api.example 01 / OPTIONS /orders · Origin + Method + Headers 02 / 204 · разрешены origin, method, headers 03 / POST /orders · credentials 04 / 200 · allow-origin + allow-credentials При credentials сервер явно разрешает origin и передачу учётных данных.Браузер / app.exampleAPI / api.example01 / OPTIONS /orders · Origin + Method + Headers02 / 204 · разрешены origin, method, headers03 / POST /orders · credentials04 / 200 · allow-origin + allow-credentialsПри credentials сервер явно разрешает origin и передачу учётных данных.
Успешный preflight разрешает только заявленные параметры будущего запроса; фактический ответ проходит отдельную CORS-проверку и повторяет заголовки для origin и учётных данных.

Что означают ответные CORS-заголовки

Access-Control-Allow-Origin разрешает браузерному коду с одного origin читать ответ. Значение * допустимо только для публичного ответа без учётных данных. Для нескольких доверенных origin сервер выбирает совпавший origin из списка и возвращает его вместе с Vary: Origin.

Access-Control-Allow-Methods отвечает на Access-Control-Request-Method в preflight. Он не заменяет маршрутизацию, аутентификацию или проверку прав: фактический запрос сервер всё равно обрабатывает по обычным правилам.

Access-Control-Allow-Headers отвечает на Access-Control-Request-Headers и разрешает заголовки фактического запроса. Он не перечисляет заголовки ответа. Сравнение имён регистронезависимо, но в конфигурации полезно сохранять единое написание.

Access-Control-Allow-Credentials: true разрешает показать ответ коду, когда запрос использует учётные данные: cookie, HTTP-аутентификацию или клиентский TLS-сертификат. Для cross-origin fetch() отправку cookie обычно запрашивают через credentials: 'include'. Одного серверного заголовка недостаточно, а политики SameSite и сторонних cookie продолжают действовать.

Сочетание Access-Control-Allow-Origin: * и Access-Control-Allow-Credentials: true браузер отвергает, когда запрос использует режим credentials: 'include'. Для cross-origin cookie возвращайте точный разрешённый origin и Access-Control-Allow-Credentials: true. Заголовок Authorization сам по себе вызывает preflight, но не переключает режим credentials на include. Если маршрут действительно публичный и не использует учётные данные, оставьте * и не добавляйте Access-Control-Allow-Credentials.

Как найти сломанный этап в DevTools

Откройте Console и Network, очистите журнал и повторите действие. Текст в Console обычно называет конкретную проверку: отсутствующий Access-Control-Allow-Origin, несовпавший origin, неразрешённый метод или заголовок, недопустимый wildcard с credentials либо сбой preflight.

Дальше разберите обмен по фактам:

  1. Сверьте origin страницы со значением Origin в запросе. Учитывайте схему, полный хост и порт; localhost и 127.0.0.1 — разные хосты.
  2. Если в Network есть OPTIONS, откройте его отдельно. Проверьте Access-Control-Request-Method, Access-Control-Request-Headers, статус и все ответные Access-Control-Allow-*.
  3. Если OPTIONS успешен, найдите фактический запрос. CORS-заголовки должны присутствовать и на его ответе, включая ответы 4xx и 5xx, которые JavaScript должен уметь прочитать.
  4. Если фактического запроса нет, исправляйте preflight. Если OPTIONS нет, запрос либо простой, либо разрешение уже взято из preflight-кеша, либо браузер остановил запрос раньше из-за другой политики или сетевого сбоя.
  5. Сравните наблюдение с серверными логами. Так видно, получил ли сервер только OPTIONS, получил ли фактический запрос и какой компонент сформировал ответ.

Общий TypeError: Failed to fetch сам по себе не доказывает CORS: Fetch так же сообщает о части сетевых и защитных отказов. Если Console не называет CORS, используйте отдельную диагностику Failed to fetch.

Где ломается настройка сервера и прокси

Чаще всего приложение правильно обрабатывает успешный API-ответ, но CORS-заголовки теряются на другой ветке. Проверьте весь путь запроса:

  • обработчик CORS выполняется до аутентификации и умеет ответить на OPTIONS без пользовательской cookie;
  • роутер разрешает OPTIONS для нужного пути, а прокси не отвечает на него собственным 404, 405 или 401;
  • балансировщик, CDN и обратный прокси не удаляют и не дублируют Access-Control-Allow-Origin;
  • редирект не переносит preflight или фактический запрос на другой origin с другой политикой;
  • CORS-заголовки добавляются к ошибочным ответам API, а не только к 2xx;
  • кеш учитывает Vary: Origin, если сервер отражает один из нескольких разрешённых origin.

Ответ сервера удобно проверить без браузера, явно воспроизведя preflight:

curl -i -X OPTIONS 'https://api.example/orders' \
  -H 'Origin: https://app.example' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization, content-type'

Эта команда проверяет статус и заголовки, но не исполняет браузерный алгоритм CORS. Успешный curl, Postman или серверный HTTP-клиент не опровергает ошибку в браузере: такие клиенты могут отправить запрос и прочитать ответ независимо от Access-Control-Allow-*.

CORS не заменяет контроль доступа

CORS исполняет браузер и решает, можно ли показать ответ JavaScript другого origin. Он не запрещает обращаться к API серверным программам и не подтверждает личность пользователя. Поэтому API отдельно проверяет аутентификацию, права на ресурс, допустимость входных данных и лимиты запросов.

Нельзя полагаться на CORS и для защиты изменяющих операций от CSRF. Часть cross-origin-запросов уходит без preflight, а сокрытие ответа не отменяет серверный эффект. Для cookie-аутентификации нужны подходящие атрибуты cookie и защита от CSRF согласно модели приложения.

Не маскируйте ошибку клиентскими обходами. mode: 'no-cors' оставляет JavaScript непрозрачный ответ без доступных статуса, заголовков и тела. Отключение браузерной защиты работает только в небезопасном локальном окружении. Публичный CORS-прокси передаёт ему запросы и секреты и добавляет чужую точку отказа. Правильное место исправления — конфигурация API или контролируемого обратного прокси с точным списком разрешённых origin.

Источники