Що таке CORS і як це працює?
CORS (Cross-Origin Resource Sharing) - це браузерний механізм безпеки, який через HTTP-заголовки дозволяє серверам контролювати, які джерела можуть читати їхні відповіді.
Теорія
TL;DR
- Браузери за замовчуванням блокують читання відповідей з інших джерел (same-origin policy, правило однакового походження). CORS дозволяє серверам явно відкрити доступ через заголовки.
- Головний заголовок -
Access-Control-Allow-Origin. Якщо він збігається з джерелом запиту, браузер передає відповідь до JavaScript. - Для складних запитів (кастомні заголовки або нестандартні методи) браузер спочатку надсилає preflight-запит
OPTIONS. - Credentials (cookies, auth-токени) потребують
Access-Control-Allow-Credentials: trueі точного джерела, не*. - Node.js CORS не перевіряє. Це суто браузерний механізм.
Короткий приклад
// Браузер на http://localhost:3000 звертається до іншого джерела
fetch('https://api.example.com/data')
.then(r => r.json())
.catch(e => console.error(e));
// Error: No 'Access-Control-Allow-Origin' header is present
// Працює, коли сервер відповідає з заголовком:
// Access-Control-Allow-Origin: http://localhost:3000Браузер блокує не сам запит, а відповідь. Запит досягає сервера, але браузер тримає відповідь, доки не перевірить заголовки.
Same-origin policy
Два URL мають однакове походження (origin), тільки якщо збігаються схема, хост і порт. http://localhost:3000 і http://localhost:3001 - різні джерела, хоча обидва на localhost.
CORS не замінює same-origin policy. Він додає шар явної згоди зверху. Сервер каже: "так, цьому джерелу можна". Браузер це поважає.
Preflight-запити
Не кожен крос-доменний запит іде напряму. Якщо запит використовує PUT, DELETE, PATCH, або нестандартний заголовок на зразок Authorization чи Content-Type: application/json, браузер спочатку надсилає запит OPTIONS.
Цей preflight питає: "Я збираюся надіслати такий запит з такого джерела. Чи можна?" Сервер відповідає заголовками Access-Control-Allow-Methods і Access-Control-Allow-Headers. Якщо все збіглось, реальний запит іде далі.
Прості запити (GET, POST, HEAD зі стандартними заголовками без JSON-тіла) preflight пропускають.
Коли що застосовувати
- Публічний API для всіх:
Access-Control-Allow-Origin: * - Додаток з власним API:
Access-Control-Allow-Origin: https://app.yourcompany.com - Запити з cookies або auth-токенами:
Access-Control-Allow-Credentials: trueі точне джерело, не* - Нестандартні запити: обробляй
OPTIONSpreflight вручну або використовуй npm-пакетcors
Типові помилки
* разом з credentials
// Неправильно - браузер відхилить цю комбінацію
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true'); // FAIL
// Правильно - точне джерело
res.header('Access-Control-Allow-Origin', req.get('Origin'));
res.header('Access-Control-Allow-Credentials', 'true');Браузер покаже: "Credential is not supported if the CORS header is *". Це найпоширеніша CORS-помилка в продакшені і класична пастка на співбесідах.
Відсутній обробник OPTIONS
// Неправильно - PUT запускає preflight, але маршруту OPTIONS немає
app.put('/data', handler);
// Правильно
app.options('*', cors()); // обробляємо preflight для всіх маршрутів
app.put('/data', cors(), handler);Preflight-запит зависає, клієнт отримує 4xx. В розробці це легко пропустити, якщо використовуєш proxy.
Помилкове припущення про прості GET-запити
Будь-який кастомний заголовок на зразок X-Request-ID робить GET нестандартним і запускає preflight. Запит виконається, але повільніше. Там, де кастомні заголовки не потрібні, не додавай їх.
Dev proxy, що ховає реальну проблему
"proxy" у package.json для Create React App обходить CORS під час розробки. Додаток деплоїться в продакшен, і все ламається. Налаштовуй CORS на сервері з першого дня.
Де застосовується
- Express:
app.use(cors({ origin: 'https://myapp.com' }))у REST API серверах - Next.js: заголовки через
NextResponseабо middleware - AWS API Gateway: налаштування CORS в консолі для кожного endpoint
- Cloudflare Workers:
response.headers.set('Access-Control-Allow-Origin', '*') - Create React App (тільки для розробки):
"proxy": "http://localhost:3001"уpackage.json
Питання для співбесіди
Q: Що таке same-origin policy?
A: Браузерне правило, яке блокує читання відповідей з інших джерел, якщо різняться схема, хост або порт. Теги <img> і <script> завантажуються з будь-якого джерела, але fetch і XMLHttpRequest без CORS-заголовків не можуть читати крос-доменні відповіді.
Q: Чим простий запит відрізняється від preflight?
A: Простий запит - це GET, POST або HEAD зі стандартними заголовками без application/json Content-Type. Все інше спочатку запускає OPTIONS preflight.
Q: Чому CORS не діє в Node.js-скриптах?
A: CORS перевіряють тільки браузери. У Node.js немає same-origin policy, тому CORS-заголовки він ігнорує. Будь-який серверний запит обходить CORS.
Q: Чи можна налаштувати CORS без бібліотеки?
A: Так. Виставляй res.header('Access-Control-Allow-Origin', origin) у middleware і окремо обробляй OPTIONS. Пакет cors просто автоматизує цей патерн.
Q: Чому mode: 'no-cors' може ламати service worker?
A: Він повертає непрозору (opaque) відповідь без статусу і тіла. Якщо service worker кешує такі відповіді, він може зберегти помилкову поруч з успішною, і розрізнити їх неможливо. Офлайн-поведінка ламається непомітно.
Приклади
Express-сервер з CORS (налаштування для розробки)
// server.js - Express на порту 3001
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
app.get('/api/user', (req, res) => res.json({ name: 'Alex' }));
app.listen(3001);// App.js - React на localhost:3000
import { useEffect, useState } from 'react';
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('http://localhost:3001/api/user')
.then(r => r.json())
.then(setUser);
}, []);
return <div>{user?.name || 'Loading...'}</div>;
// Output: Alex
}Сервер явно дозволяє джерело http://localhost:3000. Прибери цей заголовок - браузер затримає відповідь, JavaScript її не побачить, а в консолі з'явиться CORS-помилка.
Credentials і точне джерело
Якщо запит включає cookies або заголовок Authorization, * перестає працювати.
// server.js - credentials потребують точного джерела
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); // NOT *
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});// Клієнт
fetch('http://localhost:3001/api/private', {
credentials: 'include', // надсилає cookies
})
.then(r => r.json())
.then(console.log);
// Працює з точним джерелом + credentials: true
// Ламається з *: "The value of the 'Access-Control-Allow-Origin' header
// must not be the wildcard '*' when the request's credentials mode is 'include'"Бачив, як це ламає продакшен-деплої не раз. Хтось вмикає credentials: include на клієнті і забуває змінити * на реальне джерело на сервері.
Preflight у деталях
Ось що браузер насправді надсилає перед POST-запитом з JSON:
// Цей fetch автоматично запускає preflight:
fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1 }),
});
// Preflight-запит, який генерує браузер:
// OPTIONS /data HTTP/1.1
// Origin: https://yourapp.com
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: Content-Type
// Необхідна відповідь сервера:
// Access-Control-Allow-Origin: https://yourapp.com
// Access-Control-Allow-Methods: POST
// Access-Control-Allow-Headers: Content-Type
// Access-Control-Max-Age: 86400 <- кешувати результат preflight 24 годиниAccess-Control-Max-Age варто знати для співбесіди. Він каже браузеру кешувати результат preflight на N секунд, щоб OPTIONS-запит не надсилався щоразу.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.