Що таке 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.
Швидкий приклад
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.
// Неправильно - браузер видасть CORS-помилку
cors({ origin: '*', credentials: true });
// Правильно
cors({ origin: 'https://myapp.com', credentials: true });app.use(cors()) після маршрутів
Express запускає middleware по порядку. Якщо маршрути зареєстровано до cors(), відповідь іде без CORS-заголовків.
// Неправильно
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.
// Preflight не пройде - Authorization не задекларовано
cors({ origin: 'https://myapp.com' });
// Правильно
cors({
origin: 'https://myapp.com',
allowedHeaders: ['Content-Type', 'Authorization']
});localhost і 127.0.0.1 в розробці
Вказують на одну машину, але браузер вважає їх різними origin-ами. Додай обидва явно.
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.
Приклади
Базове налаштування для розробки
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() без аргументів нормально для локального тестування. Перед деплоєм замінити на явні налаштування.
Продакшн з cookie-автентифікацією
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-ів
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-ом.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.