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

JavaScript Автор: Среда и версия: Fetch API и CORS в современных браузерах
содержание

Сообщение 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.

Источники