Технологии

Ошибка CORS: blocked by CORS policy — что это и как починить

31 минАктуально на 12 сентября 2026

Ошибка CORS blocked by CORS policy

Ты подключил к игре таблицу рекордов, запустил страницу и вместо данных получил красную строку blocked by CORS policy. При этом адрес API открывается в соседней вкладке, а тот же запрос из Postman возвращает нормальный ответ. Код может быть почти правильным: доступ остановил не сервер и не интернет, а проверка внутри браузера.

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

Содержание
  1. Что такое CORS простыми словами
  2. Что такое origin и почему разные порты имеют значение
  3. Как читать ошибку has been blocked by CORS policy
  4. Почему ошибка возникает в браузере, а Postman и curl работают
  5. Простые и сложные CORS requests
  6. Как браузер решает, пропустить ответ или заблокировать
  7. Заголовки Access-Control-Allow-Origin, Methods, Headers и Credentials
  8. Как починить CORS на своей стороне
  9. Как починить CORS, если API чужой
  10. CORS и Supabase / GitHub Pages
  11. CORS не равно безопасность API
  12. Типичные ошибки новичка при настройке CORS
  13. Как проверить и диагностировать CORS по шагам
  14. Частые вопросы
  15. Короткий вывод

Что такое CORS простыми словами

CORS расшифровывается как Cross-Origin Resource Sharing — «совместное использование ресурсов между разными источниками». Ресурсом может быть список рекордов, профиль пользователя, фотография, JSON с настройками или любой другой ответ сервера. Источник, или origin, — адрес, с которого открыта страница.

Главное правило браузера звучит так: страница с одного адреса не может молча читать ответы с другого адреса. Сервер на другом адресе должен явно разрешить такое чтение с помощью специальных заголовков. Настройку этого разрешения обычно и называют CORS policy.

Допустим, игра открыта на https://player.github.io, а рекорды хранятся на https://api.example.com. JavaScript игры отправляет запрос к API. Сервер может получить запрос и даже подготовить правильный JSON, но браузер отдаст этот JSON коду игры только при подходящем разрешении CORS.

Почему браузер вмешивается? Без такого ограничения любой сайт мог бы попытаться прочитать данные из другого сайта, где ты уже вошёл в аккаунт. Ты открыл бы безобидную страницу, а её скрипт запросил бы личные данные у почты, магазина или внутренней панели компании. Браузер автоматически приложил бы доступные для этого сайта cookies, а вредоносный код попытался бы прочитать ответ.

Основой защиты служит политика одного источника — Same-Origin Policy. По умолчанию она ограничивает чтение данных между разными origin. CORS не отменяет эту защиту целиком, а даёт серверу контролируемый способ сказать: «ответ разрешено читать странице с такого-то адреса».

Из этого следуют три практических вывода:

  • CORS применяет браузер, а разрешение сообщает сервер;
  • запрос иногда доходит до сервера, хотя JavaScript не получает ответ;
  • правка одного fetch() на странице не заставит чужой сервер выдать разрешение.

CORS касается именно межсайтового чтения из браузерного JavaScript. Загрузка некоторых изображений, переход по ссылке и отправка обычной HTML-формы подчиняются своим правилам. Поэтому фраза «браузер умеет открыть этот адрес» ещё не означает «скрипт может прочитать данные по этому адресу».

Если пока неясно, чем страница отличается от API, сначала прочитай разбор что такое API. Для CORS достаточно знать: фронтенд выполняется в браузере пользователя, а бэкенд принимает запросы и возвращает данные.

Что такое origin и почему разные порты имеют значение

Origin состоит ровно из трёх частей:

  1. схема — обычно http или https;
  2. домен — например, example.com или localhost;
  3. порт — например, 3000 или 5173.

Путь после домена в origin не входит. Страницы https://example.com/game и https://example.com/profile имеют один origin. А следующие адреса относятся к разным origin:

  • http://localhost:3000 и http://localhost:5173 — отличаются порты;
  • http://example.com и https://example.com — отличаются схемы;
  • https://example.com и https://api.example.com — отличаются домены;
  • http://localhost:5173 и http://127.0.0.1:5173 — отличаются имена хоста.

Это частая причина, почему проект работал вчера и получил cors block сегодня. Например, один инструмент разработки поднял страницу на порту 3000, а после смены сборщика страница открылась на 5173. Для человека это всё тот же компьютер. Для браузера http://localhost:3000 и http://localhost:5173 — разные источники, и серверное разрешение для первого не покрывает второй.

Если порт в адресе не написан, браузер использует стандартный порт схемы. Для обычного HTTPS это один стандартный порт, для HTTP — другой. Обычно тебе не нужно указывать их в списке разрешений явно: значение заголовка Origin показывает строку, которую сервер должен проверить.

У origin нет завершающего слеша. Правильная запись разрешённого источника выглядит как https://example.com, а не https://example.com/. Также в неё не входят путь, параметры и символ #. Запись https://example.com/game нельзя использовать как способ разрешить запросы только одной странице сайта: CORS не различает пути внутри одного origin.

Самый надёжный способ узнать текущий origin — посмотреть на адрес страницы и выполнить в консоли браузера:

window.location.origin

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

Как читать ошибку has been blocked by CORS policy

Типичное сообщение в консоли выглядит примерно так:

Access to fetch at 'https://api.example.com/scores' from origin
'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Разберём его по частям.

Access to fetch at 'https://api.example.com/scores' — JavaScript пытался обратиться к этому адресу API. Сначала проверь, нет ли в адресе опечатки и тот ли это сервер.

from origin 'http://localhost:5173' — запрос сделала страница с этого origin. Именно его сервер должен разрешить. Здесь часто обнаруживается неожиданный порт или http вместо https.

has been blocked by CORS policy — браузер не дал JavaScript использовать ответ. Это не готовый диагноз причины, а результат проверки. Сетевой запрос мог завершиться ответом, перенаправлением, серверной ошибкой или не состояться как положено.

No 'Access-Control-Allow-Origin' header is present — в проверенном ответе нет заголовка, который сообщает разрешённый origin. Возможны разные причины:

  • бэкенд вообще не настроил CORS;
  • CORS настроен, но текущий origin не входит в список;
  • сервер вернул ошибку раньше, чем добавил CORS-заголовки;
  • ответ сформировал другой слой — прокси, авторизация или хостинг;
  • запрос перенаправили на страницу, где нужного заголовка нет;
  • браузер проверял ответ на OPTIONS, а обработчик этого метода отсутствует.

Фраза access blocked by CORS policy не доказывает, что нужная строка кода находится во фронтенде. Наоборот, чаще менять нужно бэкенд или настройки платформы, которая отвечает за API.

В консоли могут встречаться и более конкретные варианты:

  • заголовок содержит origin, который не совпадает с адресом страницы;
  • метод PUT или DELETE не перечислен среди разрешённых;
  • заголовок Authorization не разрешён;
  • запрос с учётными данными несовместим со значением *;
  • preflight-запрос не получил успешный ответ.

Не ограничивайся последней строкой консоли. Первая ошибка в цепочке часто полезнее: она показывает точный URL, метод или заголовок, из-за которого проверка не прошла.

Почему ошибка возникает в браузере, а Postman и curl работают

CORS — ограничение браузерной среды, а не запрет на соединение с сервером. Postman и curl являются отдельными HTTP-клиентами. Они отправляют запрос и показывают полученный ответ, но не защищают открытую веб-страницу политикой одного источника.

Поэтому возможна картина:

  • curl https://api.example.com/scores возвращает JSON;
  • Postman показывает статус 200;
  • вкладка браузера открывает тот же URL;
  • fetch() из игры завершается ошибкой blocked by CORS policy.

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

Postman полезен, чтобы отделить обычную ошибку API от CORS. Если запрос не работает и там, сначала проверь URL, метод, тело, авторизацию и состояние сервера. Разобраться с такими проверками поможет материал про тестирование API-запросов в Postman.

Можно приблизить curl к браузерной проверке, вручную отправив заголовок Origin:

curl -i https://api.example.com/scores \
  -H 'Origin: http://localhost:5173'

В ответе ищи Access-Control-Allow-Origin. Но даже с заголовком Origin команда не станет настоящим браузером: curl покажет тело ответа независимо от CORS. Решение, допускать ли ответ к JavaScript, принимает браузер.

Есть ещё одно различие. Браузер может предварительно отправить OPTIONS, а затем не отправлять основной запрос, если разрешение не получено. В Postman ты обычно сразу посылаешь POST, PUT или другой выбранный метод. Поэтому успешный основной запрос в Postman ничего не говорит об обработке OPTIONS.

Простые и сложные CORS requests

В разговорной речи CORS requests делят на простые и сложные. В стандарте используется понятие CORS-safelisted request — запрос, для которого не нужна предварительная проверка. Слово «простой» не означает, что такой запрос автоматически разрешён. Браузер всё равно проверит CORS-заголовок в ответе перед тем, как передать данные JavaScript.

К простым обычно относятся запросы с методами GET, HEAD или POST, если они используют только ограниченный набор безопасных заголовков и допустимый тип содержимого. Например, обычная HTML-форма часто отправляет POST без предварительного запроса.

Запрос перестаёт быть простым, если в нём есть хотя бы одно условие, требующее отдельного разрешения. Частые примеры:

  • метод PUT, PATCH или DELETE;
  • заголовок Authorization;
  • собственный заголовок вроде X-Game-Version;
  • Content-Type: application/json;
  • другие заголовки или параметры, не входящие в безопасный набор CORS.

JSON особенно часто удивляет новичков. Обычный запрос на сохранение рекорда может выглядеть так:

fetch('https://api.example.com/scores', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Alex', score: 42 })
})

Из-за Content-Type: application/json браузер сначала выполняет preflight — предварительный запрос методом OPTIONS. Он не сохраняет рекорд. Он спрашивает сервер: разрешены ли от этого origin метод POST и заголовок Content-Type?

В preflight браузер обычно передаёт:

  • Origin — адрес страницы;
  • Access-Control-Request-Method — метод будущего запроса;
  • Access-Control-Request-Headers — нестандартные заголовки будущего запроса.

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

Менять JSON на другой формат только ради обхода preflight обычно не нужно. Надёжнее корректно обработать OPTIONS и явно разрешить методы и заголовки, которые действительно использует приложение.

Как браузер решает, пропустить ответ или заблокировать

Схема ниже показывает основную развилку. У простого запроса браузер сразу обращается к серверу. У запроса с preflight он сначала проверяет разрешение через OPTIONS. В обоих случаях окончательное решение зависит от CORS-заголовков ответа.

flowchart TB
    A["Страница отправляет запрос"] --> B{"Нужен preflight?"}
    B -->|"Да"| C["Браузер отправляет OPTIONS"]
    B -->|"Нет"| D["Сервер отвечает"]
    C --> D
    D --> E{"Разрешающий заголовок подходит?"}
    E -->|"Да"| F["Ответ доступен JavaScript"]
    E -->|"Нет"| G["Браузер блокирует доступ"]

Фраза «блокирует запрос» не всегда технически точна. При проваленном preflight браузер действительно не отправляет основной запрос. При простом запросе сервер может выполнить действие и вернуть ответ, но браузер скроет ответ от JavaScript. Это различие критично для операций записи.

Например, игра отправляет простой POST, сервер записывает результат, но забывает Access-Control-Allow-Origin. На странице появляется ошибка, и код решает повторить запрос. В базе могут возникнуть две записи, хотя оба раза интерфейс показывал сбой. Поэтому повторять операции после CORS-ошибки наугад опасно — сначала посмотри вкладку Network и журнал сервера.

Заголовки Access-Control-Allow-Origin, Methods, Headers и Credentials

CORS управляется HTTP-заголовками ответа. Их добавляет сервер, прокси перед ним или платформа, на которой работает функция. Добавить эти заголовки в запрос из fetch() и тем самым выдать разрешение самому себе нельзя.

Заголовок ответа Что делает Типичное значение
Access-Control-Allow-Origin Разрешает конкретному origin читать ответ либо открывает ответ для любых origin без учётных данных https://game.example.com или *
Access-Control-Allow-Methods Перечисляет методы, разрешённые для межсайтового запроса GET, POST, OPTIONS
Access-Control-Allow-Headers Перечисляет заголовки, которые разрешено прислать в основном запросе Content-Type, Authorization
Access-Control-Allow-Credentials Разрешает браузеру открыть ответ запроса с cookies или другими учётными данными true

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

Звёздочка означает «любой origin», но не «разрешить вообще всё». Она не отменяет авторизацию, не создаёт доступ к базе и несовместима с браузерным запросом, который использует учётные данные через credentials: 'include'. Для такого запроса сервер должен вернуть конкретный origin и Access-Control-Allow-Credentials: true.

Access-Control-Allow-Methods сообщает, какие методы допустимы после preflight. Если приложение посылает DELETE, а сервер перечислил только GET, POST, браузер остановит операцию.

Access-Control-Allow-Headers относится к заголовкам запроса. Если фронтенд отправляет токен в Authorization, этот заголовок нужно разрешить. Сам по себе список не проверяет правильность токена — он лишь пропускает запрос к обычной серверной проверке.

Access-Control-Allow-Credentials нужен не для каждого токена. Параметр credentials в Fetch API в первую очередь управляет cookies, клиентскими сертификатами и HTTP-аутентификацией. Токен в Authorization вызывает preflight из-за заголовка, но его наличие само по себе не требует credentials: 'include'.

Когда сервер динамически возвращает разные значения Access-Control-Allow-Origin, полезно также корректно разделять кэшированные ответы по входящему Origin. Готовые CORS-библиотеки и управляемые платформы обычно решают эту деталь лучше самописного набора строк.

Как починить CORS на своей стороне

Если бэкенд принадлежит тебе, исправление делается там. Нужно определить реальные адреса фронтенда, разрешить нужные методы и заголовки, а затем убедиться, что CORS применяется и к успешным ответам, и к ошибкам, и к OPTIONS.

Перед правкой выпиши окружения приложения:

  • локальная разработка, например http://localhost:5173;
  • опубликованный сайт, например https://username.github.io;
  • отдельный тестовый домен, если он есть.

Не копируй примеры адресов как готовую настройку. Замени их на origin своей страницы. Путь проекта на GitHub Pages не включается: для страницы https://username.github.io/snake/ origin равен https://username.github.io.

Express и пакет cors

Для приложения на Express проще использовать пакет cors, а не вручную собирать заголовки. Установи его в проект бэкенда:

npm install cors

Короткая настройка для двух разрешённых origin:

import express from 'express'
import cors from 'cors'

const app = express()

app.use(cors({
  origin: [
    'http://localhost:5173',
    'https://username.github.io'
  ],
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}))

app.use(express.json())

Подключай middleware до маршрутов API, чтобы заголовки добавлялись ко всем нужным ответам. Если приложение действительно использует cookies между origin, добавь credentials: true и оставь конкретные адреса вместо *.

У серверов на CommonJS синтаксис импорта может выглядеть как require('cors'). Не проси ИИ бездумно заменить систему модулей всего проекта: пусть сначала определит, какой синтаксис уже используется.

FastAPI и CORSMiddleware

В FastAPI для той же задачи есть CORSMiddleware:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",
        "https://username.github.io",
    ],
    allow_methods=["GET", "POST"],
    allow_headers=["Content-Type", "Authorization"],
    allow_credentials=False,
)

Здесь тоже нужны реальные origin и только используемые возможности. Если фронтенд передаёт cookies, значение allow_credentials меняется на True, но список origin должен оставаться явным.

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

Что попросить у Claude Code

Хорошая задача для ИИ содержит факты, а не команду «почини CORS как-нибудь». Можно дать такой запрос:

Найди в проекте место, где создаётся HTTP-сервер и подключаются маршруты API.
Настрой CORS только для origin http://localhost:5173 и
https://username.github.io. Разреши методы GET и POST, заголовки
Content-Type и Authorization. Cookies мы не используем. Убедись, что
preflight OPTIONS обрабатывается до маршрутов и CORS-заголовки есть также
на ответах с ошибкой. Не меняй адреса API и систему модулей проекта.
После правки покажи изменённые файлы и команды проверки.

Если используются cookies, сообщи это отдельно. Если не знаешь, используются ли они, попроси ИИ сначала найти credentials: 'include', withCredentials, установку cookies и серверную сессию, а уже потом предложить настройку.

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

Как починить CORS, если API чужой

Если API принадлежит другому сервису, твой фронтенд не может самостоятельно включить allow cors. Разрешение должен вернуть владелец API. Сначала проверь документацию сервиса: возможно, у него есть отдельный браузерный API, настройка разрешённых доменов или требование использовать серверный доступ.

Если прямые cors requests из браузера не поддерживаются, обычное решение — прокси на своём сервере:

  1. страница обращается к твоему бэкенду на контролируемом адресе;
  2. твой бэкенд запрашивает чужой API как серверный клиент;
  3. бэкенд возвращает странице только нужные данные и свои CORS-заголовки.

Вместо постоянно работающего сервера можно использовать serverless-функцию — небольшой серверный обработчик, который запускается по запросу на облачной платформе. С точки зрения CORS это всё равно серверная прослойка. Она хранит секретный ключ, проверяет входные данные, обращается к чужому API и формирует безопасный ответ для фронтенда.

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

Секретный ключ чужого сервиса нельзя класть в JavaScript страницы, даже если сайт собирается из переменной окружения. Всё, что попало в браузерный пакет, пользователь может увидеть. Ключ остаётся на сервере или в секретах serverless-платформы.

Расширение, которое «отключает CORS в браузере», годится только для локальной диагностики в отдельном тестовом профиле. Оно меняет защиту на твоём компьютере, но не на компьютерах пользователей. После публикации их браузеры снова заблокируют ответ. Кроме того, ослабление браузерной защиты повышает риск на других открытых сайтах.

Запуск Chrome с отключённой веб-безопасностью имеет ту же проблему. Это не часть продукта и не способ выпуска приложения. Не проси пользователей менять защитные флаги ради твоего сайта.

CORS и Supabase / GitHub Pages

GitHub Pages размещает статические файлы: HTML, CSS, JavaScript, изображения и манифест PWA. Собственного серверного обработчика у такой страницы нет. Когда «Змейка» читает общую таблицу рекордов, запрос идёт напрямую из браузера к API Supabase, то есть между разными origin.

Это не означает, что связка обречена на cors block. Браузерный доступ — штатный сценарий для публичных API Supabase. Клиентская библиотека обращается к проекту, а ответы сервиса содержат нужные CORS-разрешения. Поэтому игра на https://username.github.io/snake/ может читать и добавлять рекорды без собственного прокси.

Разрешение CORS не заменяет правила базы. Во фронтенде используют предназначенный для публичного клиента ключ, а доступ к строкам ограничивают через Row Level Security, или RLS. Подробнее весь путь разобран в материале про общую таблицу рекордов на Supabase.

Проблемы всё же возникают в нескольких случаях:

  • игра обращается не к стандартному адресу проекта, а к своей Edge Function или внешнему API без CORS-заголовков;
  • функция обрабатывает основной метод, но не отвечает на preflight OPTIONS;
  • в коде указан неправильный URL, и запрос попадает на страницу ошибки или перенаправление;
  • собственный прокси разрешает локальный origin, но не опубликованный адрес GitHub Pages;
  • API разрешает https://username.github.io, а игра открыта через другой домен;
  • ошибка сети, авторизации или базы ошибочно принята за CORS из-за последней строки в консоли.

Для собственной Edge Function обычно нужно вернуть CORS-заголовки и отдельно обработать OPTIONS. Конкретный набор зависит от метода, заголовков и авторизации функции. Лучше попросить Claude Code сверить реализацию с актуальной официальной документацией Supabase, а не вставлять случайный фрагмент из старого ответа на форуме.

У опубликованного проекта есть ещё одна потенциальная граница: сама страница может запрещать соединение своей Content Security Policy. CSP и CORS выглядят похоже, но работают с разных сторон. CSP говорит, куда странице разрешено подключаться; CORS говорит, каким страницам сервер разрешает читать ответ. Различить сообщения поможет объяснение Content Security Policy.

Если проект ещё не опубликован, полезно заранее понимать, какой адрес у него появится и какие части останутся статическими. Это разобрано в инструкции про GitHub Pages и PWA.

CORS не равно безопасность API

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

Если поставить Access-Control-Allow-Origin: https://game.example.com, злоумышленник всё равно сможет отправить запрос напрямую, не используя JavaScript на своём сайте. Заголовок origin нельзя считать надёжным удостоверением клиента: вне браузера его можно указать вручную.

Для защиты API применяются другие механизмы:

  • ключи и токены определяют, кто обращается к серверу;
  • серверная проверка прав решает, что этому пользователю разрешено;
  • RLS в Supabase ограничивает операции со строками базы;
  • валидация проверяет типы, длину и допустимые значения входных данных;
  • rate limit ограничивает частоту запросов и снижает ущерб от автоматического спама;
  • журналы и мониторинг помогают заметить злоупотребление.

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

Обратное тоже верно: строгая авторизация не исправляет CORS. Сервер может правильно отклонять неверные токены и принимать верные, но браузер всё равно скроет ответ, если разрешающие заголовки отсутствуют. Безопасность API и разрешение межсайтового чтения — две отдельные настройки.

Не используй CORS как список «доверенных пользователей». Он описывает происхождение веб-страницы, а не личность человека. Любой посетитель разрешённого сайта имеет тот же origin.

Типичные ошибки новичка при настройке CORS

Звёздочка вместе с credentials

Комбинация Access-Control-Allow-Origin: * и запроса с credentials: 'include' не подходит. Когда в обмене участвуют cookies или другие браузерные учётные данные, сервер возвращает конкретный origin и заголовок Access-Control-Allow-Credentials: true.

Не включай credentials «на всякий случай». Если приложение использует токен в Authorization, это ещё не повод отправлять cookies. Лишняя настройка усложняет CORS и может расширить поверхность атаки.

Забытый OPTIONS

Маршрут POST /scores работает в Postman, но браузер перед ним посылает OPTIONS /scores. Сервер отвечает 404, 405, требует авторизацию или не добавляет разрешающие заголовки. Основной POST после этого не выполняется.

Middleware CORS должен срабатывать до авторизации и маршрутов либо обработка OPTIONS должна быть настроена явно. Preflight сам по себе обычно не несёт пользовательский токен так же, как основной запрос, поэтому требовать от него обычную авторизацию — частая ошибка.

Разный порт в разработке и публикации

Локально фронтенд может работать на http://localhost:5173, API — на http://localhost:3000, а опубликованная страница — на HTTPS-домене. Все эти origin нужно различать. Не заменяй список на * только потому, что порты меняются: лучше задать разрешённые окружения через конфигурацию.

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

Лишний слеш в конце origin

В конфигурацию копируют https://game.example.com/, а браузер присылает https://game.example.com. Строки не совпадают. Удали завершающий слеш и путь: origin заканчивается доменом и, если нужен, портом.

Та же проверка выявляет http вместо https, www вместо адреса без www и localhost вместо 127.0.0.1.

CORS-заголовок добавляется только к успешному ответу

API возвращает Access-Control-Allow-Origin при статусе 200, но забывает его при 401, 404 или 500. Тогда настоящая серверная ошибка скрывается за сообщением CORS. Настраивай заголовки на общем слое до обработчиков, чтобы браузер мог показать приложению и успешный ответ, и ожидаемую ошибку.

Заголовки добавлены во фронтенд

Новичок пытается записать Access-Control-Allow-Origin внутри fetch() в разделе headers. Это заголовок ответа, его обязан прислать сервер. Фронтенд не может разрешить сам себе доступ.

Более того, самодельный заголовок запроса может вызвать preflight и добавить новую ошибку. Удали его из браузерного кода и исправь серверную конфигурацию.

Неверное решение через no-cors

Параметр mode: 'no-cors' иногда убирает красную строку, но ответ становится непрозрачным. Прочитать JSON с рекордами не получится. Это режим для узких сценариев отправки ресурсов, а не универсальный обход CORS policy.

Разрешён метод, но не заголовок

В конфигурации есть POST, однако запрос отправляет Content-Type: application/json и Authorization. Preflight проверяет и метод, и заголовки. Разрешить нужно фактический набор, который виден в Access-Control-Request-Headers, не открывая лишние возможности без причины.

Как проверить и диагностировать CORS по шагам

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

1. Запиши адрес страницы и адрес API

Скопируй window.location.origin, полный URL запроса и HTTP-метод. Сравни схему, домен и порт. Если origin одинаковый, классический CORS между ними не нужен — ищи перенаправление, другой конечный адрес или неверную трактовку сообщения.

2. Найди OPTIONS и основной запрос

Если есть OPTIONS, открой его первым. Посмотри статус и заголовки ответа. Если после него нет POST, PUT или DELETE, preflight не прошёл.

Если OPTIONS отсутствует, запрос мог быть простым. Тогда открой основной запрос и проверь его ответ. Статус 200 не отменяет CORS: разрешающий заголовок должен присутствовать и совпадать.

3. Сравни Origin и Access-Control-Allow-Origin

В заголовках запроса найди Origin. В заголовках ответа — Access-Control-Allow-Origin. Конкретное значение должно совпасть со схемой, доменом и портом страницы. При запросе с credentials звёздочка не подходит.

4. Проверь методы и заголовки preflight

Сравни Access-Control-Request-Method с Access-Control-Allow-Methods, а Access-Control-Request-Headers — с Access-Control-Allow-Headers. Названия HTTP-заголовков не зависят от регистра, но сами значения и правила сервера могут быть настроены неверно.

5. Посмотри тело и статус серверной ошибки вне браузерного ограничения

Повтори запрос в Postman или curl, не публикуя секретный токен в сообщениях и скриншотах. Если сервер отвечает 401, 404 или 500, сначала исправь эту причину. Затем добейся, чтобы CORS-заголовки присутствовали и на таком ответе.

Для ручной проверки preflight подойдёт запрос такого вида:

curl -i -X OPTIONS https://api.example.com/scores \
  -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type,authorization'

Не вставляй в команду реальные секреты. Для preflight они не нужны.

6. Определи владельца ответа

Заголовки может формировать не код маршрута, а обратный прокси, API-шлюз, serverless-платформа или обработчик ошибки. Если правка Express не меняет ответ, проверь, доходит ли запрос до Express и не отвечает ли раньше другой слой.

7. Проверь оба окружения

После локальной проверки открой опубликованную версию. У неё другой origin, HTTPS и, возможно, другой адрес API. Успех на localhost не подтверждает, что разрешён GitHub Pages, и наоборот.

В результате диагностики у тебя должна получиться конкретная формулировка: «preflight с origin http://localhost:5173 получил 404», «ответ разрешает старый порт 3000» или «Authorization отсутствует в разрешённых заголовках». С такой формулировкой Claude Code сможет сделать точечную правку без опасного открытия API для всех сайтов.

Частые вопросы

Можно ли исправить blocked by CORS policy только во фронтенде?

Обычно нет. Разрешающие заголовки возвращает сервер. Во фронтенде можно исправить неверный URL, убрать лишний заголовок или перестать без причины отправлять credentials, но нельзя выдать себе разрешение от имени чужого API.

Почему API отвечает 200, но браузер всё равно пишет CORS error?

Статус 200 означает, что сервер обработал запрос. Если в ответе нет подходящего Access-Control-Allow-Origin, браузер не передаст тело ответа JavaScript. Смотри одновременно статус и CORS-заголовки.

Безопасно ли поставить Access-Control-Allow-Origin: *?

Для действительно публичных данных без cookies звёздочка иногда уместна. Она не защищает API и не подходит для запросов с credentials. Для личных данных и управляемого фронтенда лучше явно перечислить разрешённые origin.

Почему после добавления Authorization появился OPTIONS?

Authorization не входит в безопасный набор заголовков простого запроса. Браузер сначала делает preflight и спрашивает, разрешены ли этот заголовок и будущий метод. Сервер должен корректно ответить на OPTIONS.

Поможет ли VPN при ошибке CORS policy?

Обычно нет. VPN меняет сетевой маршрут и внешний IP, а CORS сравнивает origin страницы с разрешением сервера. VPN поможет только если рядом есть отдельная сетевая блокировка, но серверную настройку CORS он не исправит.

Почему всё работает локально, но ломается на GitHub Pages?

У опубликованной страницы другой origin: меняются схема, домен и исчезает локальный порт. Добавь реальный origin GitHub Pages в разрешённый список бэкенда и проверь HTTPS, preflight и конечный URL без перенаправлений.

Короткий вывод

Сообщение has been blocked by CORS policy означает не «API сломан», а «браузер не получил подходящего разрешения отдать ответ коду этой страницы». Сначала установи два origin, затем проверь OPTIONS и заголовки ответа. Если сервер твой — настрой CORS до маршрутов и авторизации. Если API чужой — используй предусмотренный браузерный способ доступа или свой серверный прокси.

Не отключай защиту у пользователей и не маскируй проблему через no-cors. Точная диагностика почти всегда укладывается в четыре вопроса: откуда открыта страница, куда идёт запрос, был ли preflight и какой Access-Control-Allow-Origin вернул сервер.

Читай дальше

Все статьи

Не просто статьи — тебя доведут до результата

В практикуме за 2499 ₽ рядом живая команда практикующих разработчиков и маркетологов: ведём по шагам до твоего работающего приложения. Не «ролики и сам разбирайся» — помогаем на каждом затыке.

Перейти к практикуму
Все статьи Ещё: технологии и архитектура