← Все статьи

SiteGuard12 мин чтения

Ошибка CORS: почему браузер блокирует запрос к API и как исправить настройки

API возвращает данные, запрос из командной строки работает, а форма на сайте показывает ошибку. В консоли появляется сообщение с CORS policy blocked или упоминанием отсутствующего Access-Control-Allow-Origin. Такое возможно даже при ответе 200: сервер обработал обращение, но браузер не разрешил JavaScript прочитать результат.

Чтобы понять, как исправить ошибку CORS, нужно восстановить конкретный разрешённый обмен между страницей и API. Для этого проверяют адреса, предварительный запрос, заголовки и передачу учётных данных. Разрешение всех источников обычно скрывает неточность настройки и может открыть лишний доступ. Ниже — последовательность диагностики, пример заголовков и условия, по которым исправление можно принять у разработчика.

Определите origin страницы и API

Origin для обычного HTTP-адреса определяется сочетанием схемы, имени хоста и порта. Путь страницы в него не входит. В руководстве MDN по CORS этот механизм описан как согласование доступа браузера к ресурсам другого origin с помощью HTTP-заголовков. Общий владелец доменов не делает два разных origin одинаковыми.

Страница открыта на https://shop.exampleСовпадает ли originПричина
https://shop.example/catalogДаИзменился только путь
https://shop.example:443/apiДа443 — стандартный порт HTTPS
https://api.shop.exampleНетДругое имя хоста
https://shop.example:8443НетДругой порт
http://shop.exampleНетДругая схема

Смотрите адрес фактически открытой страницы, а не предполагаемый основной домен. После добавления www, перехода на HTTPS или смены порта разработки источник запроса может измениться. Запишите также конечный адрес API: перенаправление на другой хост способно поменять условия обращения.

Для настройки используйте значение Origin, которое браузер действительно отправляет. Оно не содержит путь, параметры URL и завершающий слеш. Если страница запускается необычным способом и отправляет Origin: null, сначала выясните контекст. Добавлять null в разрешённый список просто ради исчезновения сообщения не следует.

Начните диагностику с Network, а не с догадок по консоли

Откройте инструменты разработчика в браузере, затем вкладку Network и воспроизведите проблему один раз на безопасной операции. Консоль помогает найти объяснение блокировки, а сетевой журнал показывает фактический обмен. Сохраните время проверки и URL, чтобы разработчик сопоставил обращение с серверным журналом.

  1. Найдите запрос приложения и проверьте его инициатор: это должен быть нужный экран или обработчик.
  2. Запишите метод, Origin, отправляемые заголовки и режим учётных данных.
  3. Посмотрите, был ли перед основным запросом OPTIONS и какой ответ он получил.
  4. Если основной запрос отправлен, изучите его статус, цепочку перенаправлений и заголовки ответа.
  5. Сравните ожидаемый бизнес-результат с тем, что действительно появилось на сервере.

Не объединяйте разные строки журнала в одну ошибку. Неудачный OPTIONS и основной POST с недоступным ответом требуют разных исправлений. Отсутствие OPTIONS тоже не доказывает неисправность: предварительная проверка нужна не всегда, а её разрешение может быть закешировано.

Не публикуйте полный сетевой экспорт без проверки содержимого. В нём могут находиться cookie, токены и данные формы. Для передачи задачи обычно достаточно обезличенного URL, метода, названий заголовков и согласованного фрагмента ответа. Секретные значения для объяснения несовпадения origin не нужны.

Когда браузер отправляет preflight OPTIONS

Preflight — предварительная проверка разрешения на основной запрос. Браузер отправляет OPTIONS к целевому ресурсу, указывает Origin, запланированный метод через Access-Control-Request-Method и названия заголовков, требующих явного разрешения, через Access-Control-Request-Headers. Это отдельный обмен, а не выполнение операции пользователя.

Типичные причины preflight: метод PUT или PATCH, заголовок Authorization, собственный заголовок приложения, Content-Type: application/json. Даже GET может потребовать предварительную проверку, если к нему добавлен такой заголовок. Само использование cookie не означает, что OPTIONS обязательно появится.

Некоторые запросы, часто называемые простыми, проходят без preflight: они должны одновременно соответствовать ограничениям метода, заголовков, их значений и других условий браузерного API. Для Content-Type среди допустимых типов находятся application/x-www-form-urlencoded, multipart/form-data и text/plain. Одного названия POST недостаточно, чтобы отнести запрос к этой группе. При разборе ориентируйтесь на реальный набор параметров.

Согласно Fetch Standard, CORS-preflight не включает учётные данные, хотя последующий запрос может их использовать. Поэтому обработчик OPTIONS должен оценить разрешённые origin, метод и заголовки без требования пользовательской сессии. Аутентификация, авторизация и проверка защищаемого действия остаются обязательными на основном запросе. Не отключайте их для всего API ради прохождения предварительной проверки.

Согласуйте метод и заголовки на правильной стороне

Основная путаница возникает между заголовками, которые браузер отправляет, и заголовками, которыми сервер разрешает доступ. Запись Access-Control-Allow-Origin в JavaScript-запросе не даёт странице разрешения прочитать ответ. Его выдаёт сервер API или согласованный слой перед ним.

ЗаголовокГде появляетсяЧто проверять
OriginЗапрос браузераТочный источник страницы
Access-Control-Request-MethodPreflightМетод будущего основного запроса
Access-Control-Request-HeadersPreflightНазвания заголовков, требующих разрешения
Access-Control-Allow-OriginОтвет APIРазрешённый источник чтения ответа
Access-Control-Allow-MethodsОтвет на preflightРазрешённые для этого обмена методы
Access-Control-Allow-HeadersОтвет на preflightРазрешённые заголовки запроса
Access-Control-Expose-HeadersОсновной ответДополнительные заголовки ответа, доступные JavaScript

Сопоставляйте разрешения с контрактом конкретного маршрута. Если изменился только заголовок трассировки, незачем разрешать все методы на всём сайте. Authorization перечисляют явно, когда приложение действительно его отправляет. Для обмена с credentials также задавайте конкретные методы и заголовки вместо попытки универсально заменить списки звёздочками.

Если JSON уже доступен, но JavaScript не читает нужный служебный заголовок, проверьте Expose-Headers. Это отдельная задача: разрешение отправить одноимённый заголовок через Allow-Headers не открывает доступ к заголовку ответа.

Отдельно проверьте credentials и cookie

Для API с сессией требуется согласовать поведение клиента и сервера. У fetch стандартный режим credentials — same-origin; для отправки подходящих cookie на другой origin обычно нужен include. Эти значения описаны в документации Request.credentials. Настройка Access-Control-Allow-Credentials на сервере не заставляет браузер самостоятельно включить cookie в запрос.

Когда используется credentials: include, ответ должен содержать конкретный допустимый origin и Access-Control-Allow-Credentials: true. Значение Access-Control-Allow-Origin: * с этим режимом не подходит, даже если разработчик считает ресурс доступным всем. Ориентируйтесь на режим запроса, а не только на наличие видимого Cookie в одной попытке.

Явно добавляемый заголовок Authorization проверяйте отдельно: режим credentials не создаёт токен вместо приложения. При этом успешный CORS не доказывает действительность сессии, достаточность прав или правильность токена. Сначала разделите отказ доступа к данным и запрет браузеру прочитать ответ.

На отправку cookie влияют также их атрибуты и политика браузера. Include не отменяет SameSite и ограничения сторонних cookie. При этом разные origin не обязательно означают разные сайты в смысле SameSite. Не меняйте свойства всех cookie одновременно с CORS: фиксируйте, какой именно механизм мешает разрешённому сценарию.

Настройте список разрешённых origin и корректное кеширование

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

Если разрешённых источников несколько, сервер проверяет полученное значение по allowlist и возвращает один совпавший origin. Не помещайте список адресов через запятую в Access-Control-Allow-Origin. Не используйте проверку «строка содержит название компании»: она не равна точному совпадению схемы, хоста и порта.

Для публичного ресурса без credentials допустим другой контракт: чтение с любых источников через Access-Control-Allow-Origin: *. Он подходит, когда владелец действительно разрешает такое использование содержимого. Не переносите это решение с публичного справочника на персональный кабинет только потому, что оба маршрута обслуживает один API.

Когда ответ зависит от Origin, добавьте Origin в Vary. Иначе промежуточный кеш может отдать варианту запроса заголовки, подготовленные для другого источника. При этом сохраните уже необходимые значения Vary, например Accept-Encoding. Vary не является разрешением CORS и не заменяет правила хранения персональных ответов.

Назначьте один понятный слой формирования политики: приложение, шлюз или прокси. Проверьте итоговый ответ снаружи. Если приложение и CDN независимо добавляют Access-Control-Allow-Origin, результат может содержать дубли или несовместимые значения. Исправление файла конфигурации ещё не доказывает, что посетитель получает именно эти заголовки.

Учебный пример заголовков для разрешённого API

Предположим, страница https://shop.example обращается к https://api.example/v1/delivery/quote. Учебная операция только рассчитывает доступность доставки и не создаёт заказ. Запрос использует POST, JSON, сессию и предусмотренный приложением X-CSRF-Token. Источник уже проверен по allowlist; токен проверяется при выполнении основного запроса.

Так может выглядеть ответ на соответствующий preflight. Это образец HTTP-заголовков для адаптации, а не готовый конфигурационный файл конкретного сервера:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://shop.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin
Cache-Control: no-store

Основной ответ также должен предоставить разрешение на чтение. Здесь клиент дополнительно использует X-Request-ID для сопоставления с журналом:

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: https://shop.example
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-ID
X-Request-ID: demo-132
Vary: Origin
Cache-Control: no-store

{"deliveryAvailable":true}

В примере 204 подтверждает успешный preflight, а 200 относится к расчёту. Разрешения на первом ответе не заменяют проверку второго. Значение true в заголовке credentials записывается именно так. Если проект не использует сессию или служебный заголовок, ненужные части не следует переносить автоматически.

Cache-Control здесь относится к обычному HTTP-кешированию. Он не заменяет управление отдельным кешем разрешений preflight, срок которых задаётся через Access-Control-Max-Age с учётом ограничений браузера. Не считайте no-store доказательством, что перед каждой следующей попыткой обязательно появится новый OPTIONS.

Для запрещённого источника этот набор не выдаётся. Решение о выполнении самого действия принимает серверная логика безопасности; отсутствие разрешающего заголовка не должно быть её единственным ограничением.

Проверьте ошибки приложения, прокси и перенаправления

Сообщение CORS иногда оказывается вторичным симптомом. Приложение ожидало JSON, но прокси вернул страницу аварии без нужных заголовков. Или API направил запрос на страницу входа другого origin. Браузер сообщает о недоступном ответе, хотя исправлять нужно ещё и исходную серверную проблему.

Сравните обычный успешный путь и путь ошибки для того же разрешённого источника. Если политика формируется только в успешном обработчике, аварийный ответ может остаться без неё. Для предназначенных клиенту ответов об ошибках настройте согласованное поведение, сохраняя корректный статус и безопасное содержимое. Подменять ошибку ответом 200 ради исчезновения сообщения нельзя.

Проследите цепочку до фактически отвечающего компонента. Признак ответа CDN, иной формат тела или перенаправление помогают выбрать правильного владельца исправления. Не меняйте все слои одновременно: после такого изменения трудно установить, какое условие действительно нарушалось.

Если журнал вообще не показывает нужного HTTP-обмена, проверьте другие причины блокировки: сетевую недоступность, TLS, смешанное содержимое или политику страницы. Общее сообщение JavaScript об ошибке загрузки не позволяет автоматически назвать любой такой случай CORS.

Почему no-cors и успешный curl не подтверждают исправление

Режим no-cors не превращает закрытый ответ в доступный JSON. В документации Request.mode описан opaque-ответ: JavaScript не получает доступ к его телу и заголовкам. Режим также ограничивает допустимые методы и заголовки запроса. Для интерфейса, которому нужно прочитать расчёт или подтверждение, это не восстановление обмена.

Командный клиент curl полезен, чтобы увидеть ответ сервера и заголовки, однако он не применяет браузерную модель CORS. Даже запрос с вручную указанным Origin лишь показывает, что вернул сервер. Окончательное подтверждение получают из настоящей страницы в целевом браузере с теми же методом, заголовками и режимом учётных данных.

Сравнивайте условия проверки. Ручное открытие URL в адресной строке, серверный запрос и fetch из страницы — разные действия. То, что API показывает JSON в отдельной вкладке, не доказывает разрешения читать его из интерфейса магазина.

Расширение, отключающее защиту браузера у разработчика, также меняет условия опыта. Оно не исправляет настройки для покупателей. Не передавайте приватные запросы через случайный публичный прокси: решение должно находиться в контролируемой архитектуре проекта.

Не считайте блокировку ответа отменой операции

Запрос без обязательного preflight может дойти до сервера и изменить состояние, хотя браузер затем скроет ответ. Если предварительная проверка прошла, основной запрос тоже может выполниться, а ошибка проявится уже при чтении его ответа. Поэтому сообщение в консоли не доказывает, что заказ, платёж или заявка не созданы.

Перед повтором значимого действия найдите его идентификатор и проверьте фактический результат. Используйте предусмотренную проектом защиту от дублей и согласованный порядок сверки. Для диагностики выбирайте вычисление без побочных эффектов либо специально подготовленные тестовые данные. Не нажимайте многократно кнопку оплаты, проверяя очередную версию заголовков.

CORS не является аутентификацией и сам по себе не заменяет защиту от CSRF. OWASP рассматривает защиту от подделки межсайтовых запросов как отдельную задачу, включая допустимые запросы без preflight. Серверу по-прежнему нужны проверка прав и предусмотренные приложением меры защиты изменяющих операций. Разрешённый origin означает возможность согласованного браузерного обмена, а не право любого пользователя выполнить любое действие.

Условный разбор: JSON не проходит после обновления формы

Допустим, форма расчёта начала передавать X-CSRF-Token, а политика API осталась прежней и разрешает только Content-Type. У страницы и API разные origin. В Network виден OPTIONS: в Access-Control-Request-Headers перечислены content-type и x-csrf-token, но ответ не разрешает второй заголовок. Основной POST в этой попытке не отправлен.

Разработчик подтверждает, что заголовок предусмотрен контрактом защищённого маршрута, и добавляет именно его в разрешённый набор. Затем повторяет безопасный расчёт из страницы. Проверка включает не только появление POST, но и доступность JSON для кода интерфейса, ожидаемый результат и сохранение проверки самого токена.

Если теперь POST возвращает 200, но на основном ответе отсутствует Access-Control-Allow-Origin, работа ещё не завершена. Исправляется формирование основного ответа для того же allowlist. Если вместо JSON приходит аварийная страница шлюза, назначается отдельное исправление доступности API. Эти результаты нельзя объединять в статус «CORS починили» без проверки пользовательского действия.

В учебном отчёте достаточно трёх зафиксированных фактов: какое условие не выполнялось, какое точечное изменение сделано и каким безопасным запросом подтверждён результат. Значения секретов в отчёт не включают. Прохождение этого сценария не объявляют доказательством работы всех остальных маршрутов.

Как принять исправление и продолжить наблюдение

Проверьте разрешённые рабочие origin и один заведомо неразрешённый источник в контролируемой среде. Для защищённого API отдельно сохраните проверку отказа без действительных полномочий. Успешный CORS должен открывать только предусмотренный обмен, сохраняя остальные ограничения.

  • Страница отправляет запрос на ожидаемый API с согласованными параметрами.
  • Необходимый preflight проходит, а основной запрос действительно выполняется.
  • JavaScript читает нужные данные и разрешённые служебные заголовки.
  • Ошибочные ответы обрабатываются предсказуемо и не маскируют серверный сбой.
  • Проверены варианты с учётными данными, кешами и поддерживаемыми браузерами.
  • Операция не создала недопустимых дублей, а лишние источники не получили доступ.

Учитывайте отдельное кеширование разрешений preflight: обычная перезагрузка страницы не гарантирует новый OPTIONS. Проверяйте исправление в контролируемых условиях и фиксируйте, что именно наблюдали. В рабочей инструкции сохраните владельца политики, разрешённые источники и процедуру проверки после изменения домена или API.

В задаче на изменение перечислите затронутые маршруты и слой конфигурации, сохраните прежнее состояние и условия возврата. После выпуска сравните фактические заголовки с согласованным образцом. Это помогает обнаружить ситуацию, когда исправление попало только на часть экземпляров либо было перекрыто настройкой другого компонента.

Для внешнего наблюдения за доступностью API изучите актуальные проверки SiteGuard. HTTP-ответ помогает оценивать работу проверяемого адреса, но не подтверждает браузерный CORS-сценарий: его отдельно проверяют из страницы, которой нужен доступ.