Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке CORS і як його налаштувати в Express.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CORS** (Cross-Origin Resource Sharing) - це браузерна політика, яка блокує запити до інших origin-ів, якщо сервер не надсилає правильні `Access-Control-*` заголовки. В Express.js для цього є пакет `cors`. ```js app.use(cors({ origin: 'https://myapp.com', allowedHeaders: ['Content-Type', 'Authorization'], credentials: true })); ``` **Головне правило:** `origin: '*'` з `credentials: true` браузер відхиляє. Якщо є куки або auth-заголовки - потрібен конкретний origin.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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()` без аргументів нормально для локального тестування. Перед деплоєм замінити на явні налаштування. ### Продакшн з cookie-автентифікацією ```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-ом.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.