CORS error: No 'Access-Control-Allow-Origin' — реальные причины
Каждый раз, когда я вижу в чате «у меня CORS», я мысленно делю это на три кучи: настоящий CORS, фронт-баг, выдающий себя за CORS, и серверная ошибка, которая случайно похожа на CORS. Различать их умеешь — экономишь часы.
Сообщение в браузере выглядит так:
Access to fetch at 'https://api.example.com/users'
from origin 'https://app.example.com' has been blocked
by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.Браузер заблокировал ответ. Не сервер. Не сеть. Конкретно браузер посмотрел на заголовки и решил, что показывать тебе содержимое нельзя. Дальше нужно понять — почему.
Что такое CORS на пальцах
Браузер не пускает JS со страницы https://app.example.com читать ответы от https://api.example.com, если сервер явно не разрешил. Разрешение — это заголовок Access-Control-Allow-Origin в ответе.
Если запрос «простой» (GET/POST с обычным Content-Type, без кастомных заголовков), браузер делает обычный запрос и проверяет заголовок ответа. Если запрос «сложный» (например, PUT, или с заголовком Authorization) — сначала отправляет preflight: OPTIONS с Access-Control-Request-*. Сервер должен ответить 200/204 и нужным набором Access-Control-Allow-*.
Реальная причина 1: сервер не отдаёт CORS-заголовки
Самое прямое: ты ставишь Express, он по умолчанию ничего про CORS не знает. Запрос с другого Origin приходит — ответа без нужных заголовков. Браузер блокирует.
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors({
origin: 'https://app.example.com',
credentials: true,
}));
app.get('/users', (req, res) => res.json([{ id: 1 }]));Если фронт сидит на нескольких origin-ах, делаешь массив или функцию:
const allowed = new Set([
'https://app.example.com',
'https://staging.example.com',
]);
app.use(cors({
origin: (origin, cb) => {
if (!origin || allowed.has(origin)) cb(null, true);
else cb(new Error('Not allowed by CORS'));
},
credentials: true,
}));Реальная причина 2: preflight уходит в 404 или 401
Это часто. Сервер вроде настроен на CORS, но preflight-запрос (OPTIONS) идёт через middleware с авторизацией, и тот возвращает 401 раньше, чем CORS-заголовки добавляются. Браузер видит ответ без Access-Control-Allow-Origin и говорит, что CORS сломан.
Проверяю в Network в DevTools: смотрю на запрос с методом OPTIONS — что в ответе. Если 401 или 404 — лечить нужно сервер: либо ставить CORS до auth, либо явно пропускать OPTIONS.
app.options('*', cors());
app.use(cors());
app.use(authMiddleware);Реальная причина 3: куки не идут
Поставил credentials: 'include' на фронте, ждёшь, что куки полетят с запросом. А они не идут, или идут — но сервер их не видит.
Чек-лист:
- на сервере
Access-Control-Allow-Credentials: true; - на сервере
Access-Control-Allow-Origin— конкретный URL, не*; - кука выставлена с
SameSite=None; Secure, иначе кросс-сайтом не пойдёт.
Главный момент — Allow-Origin: * с credentials: include не работает. Браузер откажет.
Реальная причина 4: ошибка на бэкенде, а не CORS
Это самый коварный сценарий. На сервере в обработчике падает 500 или 502, ответ уходит без CORS-заголовков (потому что middleware падает раньше). Браузер показывает CORS-сообщение, потому что технически это правда — заголовков нет. А реальная проблема — серверная ошибка.
Симптом — открываешь Network, видишь, что запрос доехал до сервера и там либо 5xx, либо вообще отвалился по таймауту. Реальную причину смотри в логах сервера, а не в DevTools браузера.
Поэтому правило: сначала чекаешь, какой статус-код у запроса, потом думаешь про CORS.
Реальная причина 5: фронт ходит по неправильному URL
Ты в коде указал https://api.example.com, а на сервере существует https://api.example.com/v1. Запрос ловит редирект 301 -> /v1. Редиректы для OPTIONS в большинстве серверов либо не обрабатываются, либо ломают CORS-цикл.
Лечится тривиально — поменять URL на правильный. Но симптом тот же: «у меня CORS». Поэтому смотри полный путь и редиректы в Network.
Что НЕ делать
Не отключать CORS в браузере на проде. На разработческой машине запускать Chrome с --disable-web-security можно для отладки, но это не починка. Это игнорирование симптома.
Не ставить Allow-Origin: * на API с куками. Это либо не сработает, либо откроет API всему миру.
Не пытаться чинить CORS на фронте. CORS — это политика сервера, фронт не может её изменить. Любое лекарство — на бэкенде или в proxy.
Когда нужен прокси
Иногда правильный путь — вообще обойти CORS. Если фронт и API сидят в одном домене (через nginx или Next API routes), CORS не нужен — это same-origin. У меня на нескольких сервисах фронт ходит на /api/..., который проксируется на внутренний бэкенд. Браузер видит запрос на тот же origin, никаких preflight'ов и заголовков.
location /api/ {
proxy_pass http://127.0.0.1:4000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}В Next можно сделать то же через rewrites:
module.exports = {
async rewrites() {
return [{ source: '/api/:path*', destination: 'https://api.example.com/:path*' }];
},
};Алгоритм, по которому я разбираю CORS
- Открыл DevTools Network. Какой статус у запроса? Если 5xx — это бэк, не CORS.
- Есть ли preflight (OPTIONS)? Что в его ответе?
- Какой
Originотправляет браузер и какойAccess-Control-Allow-Originвозвращает сервер? - Если используются куки — есть ли
Allow-Credentials: trueи реальный (не*) Origin? - Убедился, что URL правильный, без скрытых редиректов.
Девять раз из десяти причина находится в первых двух пунктах. CORS — это не страшно, это просто правила браузера, которые нужно один раз понять.