CORS deep dive: почему запросы блокируются и как настроить правильно
«Access to fetch has been blocked by CORS policy» — ошибка, которую видел каждый фронтенд-разработчик. Реакция обычно одна: поставить Access-Control-Allow-Origin: * и забыть. Но CORS — это не баг браузера, а механизм защиты. Разберёмся, как он работает и как настроить правильно, не открывая дыры.
Same-Origin Policy и зачем нужен CORS
Браузер запрещает JavaScript на одном origin (scheme + host + port) читать ответы с другого origin. Это Same-Origin Policy — защита от того, чтобы вредоносный сайт не мог от имени пользователя читать данные с вашего API (cookie отправляются автоматически).
CORS (Cross-Origin Resource Sharing) — контролируемое ослабление этой политики. Сервер явно указывает, каким origin разрешено читать его ответы.
Simple vs preflight requests
Не каждый cross-origin запрос вызывает preflight. «Простые» запросы отправляются сразу:
- Метод: GET, HEAD, POST
- Заголовки: только «безопасные» (Accept, Content-Type с ограничениями, Content-Language)
- Content-Type: только
text/plain,multipart/form-data,application/x-www-form-urlencoded
Всё остальное — preflight. Браузер сначала отправляет OPTIONS:
# Preflight запрос
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
# Ответ сервера
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Max-Age — кэш preflight. Без него браузер шлёт OPTIONS перед каждым запросом.
Заголовки CORS
// Express с пакетом cors — типичная настройка
import cors from 'cors';
app.use(cors({
// Конкретные origin, не wildcard
origin: ['https://app.example.com', 'https://admin.example.com'],
// Разрешить отправку cookies/Authorization
credentials: true,
// Какие методы разрешены
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
// Какие заголовки клиент может отправлять
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
// Какие заголовки клиент может читать из ответа
exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
// Кэш preflight на 24 часа
maxAge: 86400
}));
Ключевые заголовки ответа:
Access-Control-Allow-Origin— какой origin может читать ответAccess-Control-Allow-Credentials— разрешены ли cookies/auth headersAccess-Control-Allow-Headers— какие заголовки можно отправлятьAccess-Control-Allow-Methods— какие HTTP-методы разрешеныAccess-Control-Expose-Headers— какие заголовки ответа видны JS
Типичные ошибки
1. Wildcard + credentials — спецификация запрещает Access-Control-Allow-Origin: * вместе с credentials: true. Браузер заблокирует:
// ОШИБКА: не работает с credentials
app.use(cors({ origin: '*', credentials: true }));
// ПРАВИЛЬНО: динамический origin из whitelist
app.use(cors({
origin: (origin, callback) => {
const whitelist = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || whitelist.includes(origin)) {
callback(null, origin);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
2. Missing headers в preflight — клиент отправляет Authorization, но сервер не включил его в Access-Control-Allow-Headers. Preflight проходит, но основной запрос блокируется.
3. Redirect на cross-origin — если API отвечает 301/302 на другой origin, браузер блокирует. CORS не следует за редиректами.
4. Vary: Origin — если origin динамический, обязательно добавьте Vary: Origin в ответ. Иначе CDN/прокси закэширует ответ для одного origin и отдаст другому.
Настройка: Fastify и nginx
// Fastify с @fastify/cors
import fastifyCors from '@fastify/cors';
fastify.register(fastifyCors, {
origin: ['https://app.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE']
});
Nginx — когда CORS настраивается на уровне прокси:
# nginx.conf
location /api/ {
proxy_pass http://backend:3000/;
# CORS headers
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Vary "Origin" always;
# Preflight
if ($request_method = OPTIONS) {
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
return 204;
}
}
Отладка: когда CORS не при чём
Прежде чем чинить CORS, убедитесь что проблема в нём:
# Проверить заголовки ответа
curl -I -H "Origin: https://app.example.com" https://api.example.com/users
# Проверить preflight
curl -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
https://api.example.com/users
В DevTools → Network → выберите запрос → вкладка Headers. Ищите Access-Control-* заголовки в Response Headers. Если их нет — сервер не отвечает CORS-заголовками.
Частые ложные срабатывания:
- Сервер вернул 500 — браузер показывает CORS-ошибку, потому что в ответе нет заголовков
- Сеть недоступна — тоже выглядит как CORS
- Mixed content (HTTP с HTTPS-страницы) — блокируется до CORS
CORS защищает пользователей, не усложняет жизнь разработчикам. Настройте один раз правильно — с конкретными origin, credentials и Vary — и забудьте о загадочных ошибках в консоли.