Запрос из браузера блокируется
Как отличить настоящую ошибку CORS от отказа по origin, от политики безопасности содержимого и от блокировщика — и что именно исправлять в каждом случае.
На этой странице
Под «блокируется CORS» на практике скрываются пять разных вещей, и лечатся они
по-разному. Четыре из них видны в консоли браузера, а пятая — самая коварная —
в консоли не видна вообще: запрос уходит, получает 403 и тихо теряется.
Начните с различения. Дальше — разбор каждого случая с проверкой и исправлением. Настройка списка origin и директив политики безопасности описана в CORS и разрешённых origin; здесь — диагностика, когда уже не работает.
Что это на самом деле
| Что видно | Кто отказал | Куда идти |
|---|---|---|
| Сообщение в консоли про политику общего доступа, ответа в сетевой панели нет | браузер: у ответа нет разрешения | заблокирован политикой общего доступа |
| Сообщение в консоли про предварительный запрос | браузер: OPTIONS не разрешил основной запрос |
не прошёл предварительный запрос |
В сетевой панели 403, в консоли тихо |
сервер: origin не в списке токена | отказ по origin |
Сообщение про Content Security Policy |
браузер: ваша собственная политика страницы | блокировка политикой безопасности содержимого |
| Запроса нет вовсе или он отменён без ответа | расширение или блокировщик | блокировка расширением или блокировщиком |
Полезно знать заранее: запросы приёма событий отвечают разрешением для любого
домена. Транспорт намеренно открыт, а решение принимает сервер — по типу ключа,
области действия и точному origin. Поэтому настоящая ошибка CORS на
/v1/track почти всегда означает, что запрос ушёл не туда или не дошёл до
ActionPulse.
Заблокирован политикой общего доступа
Вероятная причина. Ответ, который получил браузер, не несёт заголовка с разрешением для вашего origin. Варианты по убыванию частоты:
| Причина | Признак |
|---|---|
| Запрос ушёл на адрес кабинета вместо адреса приёма | в ответе 404, путь /v1/track без базы приёма |
| Перед приёмом стоит свой прокси или CDN, который срезает заголовки ответа | в ответе нет Access-Control-Allow-Origin, хотя запрос дошёл |
| Запрос идёт к пути вне публичного набора | это /v1/t.js, /v1/t-replay.js, /v1/t-diag.js, /v1/t-picker.js или /healthz |
Запрос отправлен с credentials: 'include' |
приём никогда не выставляет разрешение на передачу cookie |
Публичное разрешение для любого домена действует на пять адресов записи
(/v1/track, /v1/identify, /v1/alias, /v1/replay,
/v1/picker/capture), на публичную конфигурацию сбора /v1/cfg и на
версионные файлы SDK вместе с манифестом. Совместимый адрес скрипта и проверка
живости в этот набор не входят — для них заголовки задаёт конфигурация
установки.
Как проверить. Запросите тот же адрес из терминала с заголовком Origin и
посмотрите заголовки ответа:
curl -sS -o /dev/null -D - \
-X POST "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Origin: https://example.com" \
-H "Authorization: Bearer YOUR_BROWSER_TOKEN" \
-d '{"batch":[]}'
Ожидаемо: Access-Control-Allow-Origin: *, Vary: Origin и статус 422
(batch is empty) — значит транспорт в порядке, ключ принят, а запрос ничего не
записал. Если заголовка разрешения нет — ответ пришёл не от приёма.
Безопасное исправление. Сверьте адрес приёма с экраном установки трекера в
кабинете: у приёма своя база, он не живёт под /api/v1. В теге это
атрибут data-api-host, в вызове инициализации — параметр хоста. Если перед
приёмом стоит ваш прокси, настройте его на проброс заголовков ответа без
изменений. Cookie приёму не нужны: уберите credentials: 'include'.
Если не помогло. Проверьте, не переписывает ли промежуточный узел код
ответа: ошибка от прокси приходит без конверта error и без заголовков
разрешения, из-за чего браузер сообщает про CORS, хотя причина — отказ прокси.
Не прошёл предварительный запрос
Вероятная причина. Браузер сначала спрашивает разрешение запросом
OPTIONS, и основной запрос не уходит, пока ответ его не разрешит. Причины:
| Причина | Признак |
|---|---|
| К запросу добавлен свой заголовок | в консоли назван заголовок, которого нет в разрешённом списке |
Метод не POST |
разрешены только POST и OPTIONS |
OPTIONS не доходит: прокси, WAF или балансировщик его не пропускают |
в сетевой панели OPTIONS с ошибкой, 403, 405 или без ответа |
OPTIONS отправлен на совместимый адрес скрипта |
у /v1/t.js зарегистрирован только GET, поэтому OPTIONS даёт 405; у версионных файлов он есть и отвечает 204 |
Разрешённый список заголовков фиксирован и никогда не расширяется по
запросу браузера: Authorization, Content-Type, X-PP-Tracker,
X-PP-Source, X-PP-SDK-Name, X-PP-SDK-Version, X-PP-App-Version,
X-PP-Release.
Как проверить. Повторите предварительный запрос руками и сравните запрошенные заголовки с разрешёнными:
curl -sS -i -X OPTIONS "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,content-type"
Ожидаемо 204 и Access-Control-Allow-Headers с перечисленным выше списком.
Безопасное исправление. Уберите свои заголовки из запросов к приёму —
идентификатор трассировки, X-Requested-With и подобные. Всё, что нужно
передать, кладите в свойства события. Если OPTIONS не пропускает ваш
периметр, разрешите метод OPTIONS для путей приёма: он не несёт данных и
отвечает до авторизации.
Если не помогло. Учтите, что предварительный запрос есть не у всех
отправок. На выходе со страницы обычные события уезжают простым запросом с
типом text/plain и токеном в теле — предварительной проверки у него нет
вовсе, а результат недоступен странице. Если события теряются только при уходе
со страницы, дело не в предварительном запросе.
Отказ по origin
Вероятная причина. Транспорт разрешён, а сервер отказал: origin страницы не совпал со списком, заданным при выпуске браузерного токена.
{ "error": { "code": "credential_origin_forbidden",
"message": "browser credential origin is not allowed", "details": {} } }
Это не ошибка CORS. Ответ приходит с разрешением для любого домена, поэтому страница его читает и браузер молчит. Трекер такой ответ не повторяет и считает партию доставленной: события исчезают без единой записи в консоли. Сравнение идёт на точное совпадение, маски не поддерживаются, а поддомен, схема и непустой порт дают разные origin.
Отдельный признак того же отказа: публичная конфигурация сбора отвечает 200 и
«всё выключено» — автосбор, записи сессий и диагностика выключены. Так же
выглядит ответ для неизвестного, отозванного или истёкшего ключа: существование
ключа не раскрывается.
Как проверить. Во вкладке «Сеть» откройте неудачный запрос на
/v1/track и посмотрите заголовок запроса Origin — это точное
значение, с которым идёт сравнение. Затем сравните его со столбцом
«Scopes / origins» в разделе «Credentials проекта» настроек проекта. Тот же
отказ воспроизводится из терминала:
curl -sS -o - -w '\n%{http_code}\n' \
-X POST "https://YOUR_ACTIONPULSE_INGEST_HOST/v1/track" \
-H "Origin: https://example.com" \
-H "Authorization: Bearer YOUR_BROWSER_TOKEN" \
-d '{"batch":[]}'
Подставьте свой origin: 422 — origin принят, 403 — нет.
Безопасное исправление. Список origin у выпущенного токена не редактируется
— ни правкой, ни ротацией. Выпустите новый браузерный токен, перечислив все
адреса сразу: production, варианты с www и без, поддомены со своими
страницами, адрес предпросмотра сборок. Замените значение в теге, убедитесь,
что события идут, и только потом отзовите старый токен.
Если не помогло. Посмотрите, не приходит ли Origin: null. Так выглядят
непрозрачные origin: страница из локального файла, песочница iframe без
allow-same-origin, документ из data:-адреса. Такой origin не совпадает ни с
одним адресом в списке и всегда даёт 403. Полный разбор причин — в
ключах и origin.
Блокировка политикой безопасности содержимого
Вероятная причина. Отказывает не ActionPulse, а политика вашей же страницы. Директив нужно три, и каждая ломается по-своему:
| Директива | Что перестаёт работать | Сообщение в консоли |
|---|---|---|
script-src без origin файлов трекера |
ничего не загружается вовсе | отказ загрузить скрипт |
connect-src без origin приёма |
скрипт работает, события не уходят | отказ подключиться к адресу |
worker-src без blob: |
только записи сессий | отказ создать воркер |
Отдельный случай — политика с одноразовым числом (nonce) или с хешами:
дополнительные модули трекера подключаются динамически, своего nonce у них
нет, поэтому основной файл загружается, а автосбор, записи и диагностика молча
не включаются. Ошибка загрузки модуля не является ошибкой сбора: базовые
события продолжают идти.
Как проверить. Откройте консоль и найдите сообщения про
Content-Security-Policy: в них названа и нарушенная директива, и адрес.
Проверьте по сетевой панели, что загрузилось: файл ядра, файлы модулей, запрос
/v1/cfg, запрос /v1/track. Первый отсутствующий в этой цепочке и есть
заблокированный.
Безопасное исправление. Разрешите ровно то, что нужно, не ослабляя политику целиком:
script-src 'self' https://YOUR_ACTIONPULSE_HOST;
connect-src 'self' https://YOUR_ACTIONPULSE_HOST;
worker-src 'self' blob:;
Для политики с nonce добавьте origin файлов трекера в script-src явно либо
используйте 'strict-dynamic', который распространяет доверие на скрипты,
добавленные уже доверенным скриптом. Директива unsafe-eval не нужна ни при
каких условиях: в бандлах нет ни eval, ни new Function. Новую политику
удобно сначала выкатить в режиме отчётов, а не выключать сбор.
Если не помогло. Если файлы вы отдаёте
со своего домена, в script-src достаточно 'self',
но connect-src всё равно должен содержать адрес приёма: события уходят на
приём, а не на ваш домен. И проверьте worker-src отдельно — про него забывают
чаще всего, а симптом обманчиво частичный.
Блокировка расширением или блокировщиком
Вероятная причина. Запрос не отклонён — он не создан. Блокировщики рекламы и трекинга работают по спискам адресов и отменяют запрос до отправки; часть расширений дополнительно вырезает сам файл скрипта из разметки.
Отличительные признаки: запроса нет в сетевой панели вовсе или он помечен как отменённый и не имеет ни кода ответа, ни заголовков. В консоли — сообщение о блокировке клиентом, а не о политике общего доступа.
Как проверить. Откройте страницу в приватном окне без расширений или в другом профиле браузера. Если запросы появились — дело в расширении. Дополнительно сравните две записи в сетевой панели: у отказа сервера всегда есть код ответа, у блокировки клиентом кода нет.
Безопасное исправление. На своей стороне это не исправляется настройкой ActionPulse, и не нужно пытаться обойти решение посетителя. Что действительно помогает: отдавать файлы трекера со своего домена — тогда файл не выглядит как сторонний скрипт. Часть блокировщиков всё равно закроет отправку событий на адрес приёма; это нормальная часть картины, и её нужно учитывать при сверке данных с другими источниками.
Если не помогло. Не считайте потери от блокировщиков ошибкой интеграции. Проверить корректность разметки можно и на устройстве без расширений, а подтверждённые бизнес-факты — оплаты, заказы, регистрации — отправляйте с бэкенда: серверные события блокировщики не видят в принципе.
Что проверить
- Адрес приёма в теге или в инициализации совпадает с адресом на экране установки трекера, и это не адрес кабинета.
- Предварительный запрос
OPTIONSна/v1/trackотвечает204, а вAccess-Control-Allow-Headersесть все заголовки, которые вы отправляете. - Своих заголовков к запросам приёма нет.
- Заголовок запроса
Originприсутствует в списке origin токена — буква в букву, включая схему и порт. - В консоли нет сообщений про
Content-Security-Policy, аworker-srcпроверен отдельно от остальных директив. - Проверка выполнена в окне без расширений — иначе половина выводов будет о блокировщике, а не об интеграции.