lenec ru

← все посты

CORS deep dive: почему запросы блокируются и как настроить правильно

15K

«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 headers
  • Access-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 — и забудьте о загадочных ошибках в консоли.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.