Skip to main content

Що таке CORS і як його налаштувати в Express.js?

CORS (Cross-Origin Resource Sharing) - це браузерна політика безпеки, яка блокує JavaScript-запити до інших origin-ів, якщо сервер не дозволяє їх явно через HTTP-заголовки відповіді.

Теорія

TL;DR

  • CORS перевіряє браузер, а не сервер. Postman і curl ігнорують його повністю.
  • Різні origin-и: будь-яка різниця в протоколі, домені або порті.
  • Перед складними запитами браузер автоматично надсилає preflight (OPTIONS), щоб дізнатись, що сервер дозволяє.
  • origin: '*' з credentials: true браузер відхиляє. Потрібен конкретний origin.
  • В продакшні: origin - URL фронтенду, credentials: true для кук, allowedHeaders якщо є Authorization.

Швидкий приклад

js
const express = require('express'); const cors = require('cors'); const app = express(); // Dev: дозволяємо всі origin-и app.use(cors()); // Продакшн: обмежуємо конкретним фронтендом app.use(cors({ origin: 'https://myapp.com', methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, // потрібно для кук та auth-заголовків maxAge: 86400 // кешуємо preflight на 24 години })); app.get('/api/data', (req, res) => res.json({ ok: true })); app.listen(3000);

cors() без аргументів виставляє Access-Control-Allow-Origin: * і обробляє OPTIONS preflight-и автоматично. З налаштуваннями - тільки вказаний origin отримує цей заголовок назад.

Що вважається іншим origin

Origin - це протокол + домен + порт. Всі три мають збігатись. https://myapp.com і http://myapp.com - різні origin-и. myapp.com:3000 і myapp.com:4000 теж. Навіть localhost:3000 і 127.0.0.1:3000 браузер вважає різними origin-ами, що часто ставить в тупик під час локальної розробки.

Як працює preflight

Коли JavaScript надсилає "не простий" запит (будь-що з заголовком Authorization, або PUT/DELETE), браузер не відправляє його одразу. Спочатку йде preflight: автоматичний OPTIONS-запит з переліком методу і заголовків.

Браузер → OPTIONS /api/login Access-Control-Request-Method: POST Access-Control-Request-Headers: Authorization Сервер → 200 OK Access-Control-Allow-Origin: https://myapp.com Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: Authorization

Якщо цих заголовків у відповіді немає або вони неправильні, реальний запит не піде. Пакет cors обробляє це автоматично. При ручному middleware потрібна явна перевірка req.method === 'OPTIONS'.

Коли що використовувати

  • Локальна розробка: app.use(cors()) без налаштувань.
  • Один фронтенд: origin: 'https://yourapp.com'.
  • Кілька фронтендів: масив або callback-функція валідації.
  • Куки або auth-токени: credentials: true з конкретним origin, не '*'.
  • Публічний API без авторизації: origin: true (повертає origin запиту назад), без credentials.

Типові помилки

origin: '*' разом з credentials: true

Браузери відхиляють цю комбінацію. Специфікація забороняє wildcard-origin при передачі credentials.

js
// Неправильно - браузер видасть CORS-помилку cors({ origin: '*', credentials: true }); // Правильно cors({ origin: 'https://myapp.com', credentials: true });

app.use(cors()) після маршрутів

Express запускає middleware по порядку. Якщо маршрути зареєстровано до cors(), відповідь іде без CORS-заголовків.

js
// Неправильно app.get('/api/data', handler); app.use(cors()); // Правильно app.use(cors()); app.get('/api/data', handler);

Не вказано allowedHeaders для Authorization

Authorization - не "простий" заголовок. Він запускає preflight, і сервер має перелічити його в Access-Control-Allow-Headers. Без цього preflight провалюється і реальний запит не проходить. Це найпоширеніша CORS-проблема на Stack Overflow.

js
// Preflight не пройде - Authorization не задекларовано cors({ origin: 'https://myapp.com' }); // Правильно cors({ origin: 'https://myapp.com', allowedHeaders: ['Content-Type', 'Authorization'] });

localhost і 127.0.0.1 в розробці

Вказують на одну машину, але браузер вважає їх різними origin-ами. Додай обидва явно.

js
origin: ['http://localhost:3000', 'http://127.0.0.1:3000']

Де зустрічається в реальних проектах

  • Create React App: proxy в package.json для локальної розробки, Express CORS для продакшну.
  • Next.js: res.setHeader('Access-Control-Allow-Origin', ...) в API routes або спільний об'єкт corsOptions.
  • NestJS: app.enableCors({ origin: process.env.ALLOWED_ORIGINS }) в main.ts.
  • Strapi: конфіг cors в config/middlewares.js.

Питання на співбесіді

Q: Що таке preflight-запит?
A: Автоматичний OPTIONS-запит, який браузер надсилає перед "не простими" запитами. Він перевіряє, чи дозволяє сервер метод і заголовки, до того як піде реальний запит.

Q: Чому Postman працює, а браузер видає помилку?
A: Postman не є браузером і не застосовує Same-Origin Policy. Він надсилає запити напряму, без перевірки CORS. Це суто браузерний механізм.

Q: Як дебажити CORS у Chrome?
A: DevTools - вкладка Network, шукай OPTIONS-запит зі статусом 0 або 403. В консолі буде "No 'Access-Control-Allow-Origin' header". Очисти кеш під час тестування, щоб preflight не кешувався.

Q: Чи можна credentials: true з кількома origin-ами?
A: Не зі статичним рядком. Браузер вимагає один конкретний origin у заголовку відповіді, коли передаються credentials. Потрібен callback: origin: (o, cb) => cb(null, allowed.includes(o) ? o : false).

Q: Що таке opaque response?
A: При fetch з mode: 'no-cors' браузер повертає opaque response: немає тіла, статусу чи заголовків. Підходить для зображень або скриптів з зовнішніх джерел, але марно для JSON API.

Приклади

Базове налаштування для розробки

js
const express = require('express'); const cors = require('cors'); const app = express(); app.use(cors()); // Access-Control-Allow-Origin: * для всіх маршрутів app.get('/api/status', (req, res) => { res.json({ status: 'ok' }); }); app.listen(3000);

cors() без аргументів нормально для локального тестування. Перед деплоєм замінити на явні налаштування.

js
const express = require('express'); const cors = require('cors'); const app = express(); const corsOptions = { origin: process.env.FRONTEND_URL || 'https://myapp.com', methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, // дозволяє браузеру надсилати куки та auth-заголовки maxAge: 86400 // кешує preflight на 24 години - менше зайвих roundtrip-ів }; app.use(cors(corsOptions)); app.use(express.json()); app.post('/api/login', (req, res) => { res.cookie('token', 'jwt-here', { httpOnly: true, secure: true }); res.json({ user: 'logged-in' }); }); app.listen(3000);

На стороні React: fetch('/api/login', { method: 'POST', credentials: 'include' }). Обидві сторони мають погодитись: credentials: true на сервері і credentials: 'include' на клієнті. Якщо одна зі сторін не вказана, куки не передаються.

Динамічна валідація кількох origin-ів

js
const allowedOrigins = [ 'https://myapp.com', 'https://admin.myapp.com', 'http://localhost:3000', 'http://127.0.0.1:3000', ]; app.use(cors({ origin: (origin, callback) => { // дозволяємо server-to-server запити і Postman (без заголовка Origin) if (!origin) return callback(null, true); if (allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error(`Origin ${origin} не дозволено`)); } }, credentials: true, }));

Callback повертає один конкретний origin для кожного запиту - це те, що браузер вимагає при credentials: true. Я бачив проекти, де пропускали перевірку !origin, а потім дивувались, чому health checks в CI падають: такі запити приходять без заголовка Origin і блокуються callback-ом.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?