Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке CORS і як це працює?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CORS (Cross-Origin Resource Sharing)** - це браузерний механізм, який через HTTP-заголовки контролює, які джерела можуть читати відповіді сервера. Сервер виставляє `Access-Control-Allow-Origin`; браузер перевіряє його перед тим як передати відповідь до JavaScript. Складні запити спочатку проходять preflight-перевірку `OPTIONS`. Для credentials потрібне точне джерело, не `*`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 не перевіряє. Це суто браузерний механізм. ### Короткий приклад ```js // Браузер на 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` і точне джерело, не `*` - Нестандартні запити: обробляй `OPTIONS` preflight вручну або використовуй npm-пакет `cors` ### Типові помилки **`*` разом з credentials** ```js // Неправильно - браузер відхилить цю комбінацію 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** ```js // Неправильно - 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 (налаштування для розробки) ```js // 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); ``` ```jsx // 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`, `*` перестає працювати. ```js // 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(); }); ``` ```js // Клієнт 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: ```js // Цей 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`-запит не надсилався щоразу.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.