CORS и ошибки запросов #
CORS (Cross-Origin Resource Sharing) — механизм браузера, который ограничивает чтение ответов на запросы к другому origin (другой домен, порт или протокол). JavaScript на https://shop.example не может свободно читать ответ от https://api.other.com, пока сервер явно не разрешит это заголовками.
Origin и «тот же сайт» #
Origin = протокол + хост + порт:
| URL страницы | URL API | Кросс-домен? |
|---|---|---|
https://jsflow.dev/app | https://jsflow.dev/api | Нет |
https://jsflow.dev/app | http://jsflow.dev/api | Да (протокол) |
https://jsflow.dev/app | https://api.jsflow.dev | Да (хост) |
// same-origin — CORS не мешает читать ответ
const home = await fetch('/api/config')
const config = await home.json()
Запрос уходит на другой origin, но если сервер не прислал разрешающие заголовки, скрипт не увидит тело ответа — в консоли типичная ошибка про CORS policy.
Что видит разработчик #
Симптомы на фронтенде:
- В Network вкладке запрос есть, статус может быть 200.
- В Console — сообщение, что доступ к ответу заблокирован CORS.
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 и таймаутов.