CORS и ошибки запросов

CORS и ошибки запросов #

CORS (Cross-Origin Resource Sharing) — механизм браузера, который ограничивает чтение ответов на запросы к другому origin (другой домен, порт или протокол). JavaScript на https://shop.example не может свободно читать ответ от https://api.other.com, пока сервер явно не разрешит это заголовками.


Origin и «тот же сайт» #

Origin = протокол + хост + порт:

URL страницыURL APIКросс-домен?
https://jsflow.dev/apphttps://jsflow.dev/apiНет
https://jsflow.dev/apphttp://jsflow.dev/apiДа (протокол)
https://jsflow.dev/apphttps://api.jsflow.devДа (хост)
// same-origin — CORS не мешает читать ответ
const home = await fetch('/api/config')
const config = await home.json()

Запрос уходит на другой origin, но если сервер не прислал разрешающие заголовки, скрипт не увидит тело ответа — в консоли типичная ошибка про CORS policy.


Что видит разработчик #

Симптомы на фронтенде:

  1. В Network вкладке запрос есть, статус может быть 200.
  2. В Console — сообщение, что доступ к ответу заблокирован CORS.
  3. fetch отклоняется; response недоступен для чтения.
try {
  const response = await fetch('https://api.other.com/v1/rates')
  const data = await response.json()
} catch (err) {
  // TypeError: Failed to fetch — часто CORS или сеть
  console.error(err.message)
}

Это не баг fetch и не «сломанный API» с точки зрения сервера: сервер мог ответить, но браузер защищает пользователя от чтения чужих данных чужим сайтом.


Простые и «непростые» запросы #

Браузер может отправить preflight — предварительный OPTIONS-запрос — перед основным, если запрос считается непростым:

  • метод не GET/HEAD/POST;
  • нестандартные заголовки (например, Authorization);
  • Content-Type, кроме application/x-www-form-urlencoded, multipart/form-data, text/plain.
await fetch('https://api.partner.com/sync', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer token-abc',
  },
  body: JSON.stringify({ lastSync: Date.now() }),
})

Сначала уйдёт OPTIONS. Сервер должен ответить заголовками вроде Access-Control-Allow-Methods и Access-Control-Allow-Headers. Без них основной запрос браузер не выполнит так, как ожидает фронтенд.


Заголовки, которые решает сервер #

Клиентский JS не может обойти CORS, выставив «свои» CORS-заголовки — браузер их проигнорирует или запретит.

Типичный набор на стороне API:

Access-Control-Allow-Origin: https://shop.example
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization

Для cookie и авторизации между сайтами добавляют Access-Control-Allow-Credentials: true и не используют * в Allow-Origin.

Настройка — задача бэкенда или прокси. Фронтенд может только выбрать same-origin URL или сообщить бэкенду нужный origin.


Обходы и антипаттерны #

ПодходКомментарий
Same-origin API/api на том же домене — CORS не нужен
Прокси на своём сервереБраузер → ваш backend → чужой API
Публичные JSONPУстарело, небезопасно, не используйте в новом коде
Отключить CORS в браузереТолько для локальной отладки, не для пользователей
// dev: фронт на localhost:5173, API на localhost:3000 — разные порты = разный origin
// решение: прокси в vite/webpack или общий gateway
const response = await fetch('/api/tracks') // проксируется на backend

Другие причины «Failed to fetch» #

Не каждая ошибка — CORS:

  • Нет сети, DNS, SSL-сертификат.
  • Mixed content — HTTPS-страница запрашивает HTTP.
  • Блокировщики или корпоративный firewall.
  • Таймаут — у fetch нет встроенного timeout; нужен AbortSignal.
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 8000)

try {
  const response = await fetch('/api/slow', { signal: controller.signal })
  clearTimeout(timer)
} catch (err) {
  if (err.name === 'AbortError') {
    console.error('Превышено время ожидания')
  }
}

Сначала смотрите вкладку Network: статус, тип ошибки, был ли preflight.


Итог #

  • CORS — политика браузера, не JavaScript и не fetch.
  • Кросс-origin ответ недоступен JS, пока сервер не разрешит origin заголовками.
  • Preflight (OPTIONS) срабатывает на «сложные» запросы с нестандартными методами и заголовками.
  • Исправление на продакшене — настройка API или same-origin прокси, не отключение безопасности у пользователя.
  • «Failed to fetch» также бывает из-за сети, mixed content и таймаутов.

См. также #

Часто спрашивают

Почему fetch «падает» на чужом домене?
Браузер блокирует чтение ответа без CORS-заголовков с сервера. На фронте это выглядит как сетевая ошибка TypeError. Подробнее
Когда нужен WebSocket, а когда SSE?
WebSocket — двусторонний канал; SSE — односторонний поток событий с сервера проще для подписок «сервер → клиент». Подробнее