Пошук уроків, статей та іншого контенту
Розберете CORS, preflight-запити та причини обмежень браузера під час звернення до іншого домену.
Браузер застосовує політику Same-Origin Policy — політику одного джерела. Вона обмежує взаємодію сторінки з ресурсами, які належать іншому джерелу.
Джерело визначається трьома частинами:
протоколом: http або https;
доменом;
портом.
Наприклад, ці адреси мають різні джерела:
https://app.example.com
https://api.example.com
http://app.example.com
https://app.example.com:8443Навіть різні піддомени вважаються різними джерелами. Якщо вебзастосунок відкрито з:
http://localhost:3000а API працює на:
http://localhost:4000це кросдоменний запит, оскільки порти відрізняються.
CORS (Cross-Origin Resource Sharing) — механізм, який дозволяє серверу явно повідомити браузеру, які кросдоменні запити дозволені.
Важливо розділяти дві ролі:
браузер вирішує, чи дозволити JavaScript прочитати відповідь;
сервер вирішує, які джерела, методи й заголовки він дозволяє.
Наприклад, сторінка з https://frontend.example.com може виконати:
const response = await fetch("https://api.example.com/users");
const users = await response.json();Але браузер дозволить коду прочитати відповідь лише тоді, коли API поверне відповідний заголовок:
Access-Control-Allow-Origin: https://frontend.example.comЯкщо заголовка немає або його значення не відповідає джерелу сторінки, браузер заблокує доступ до відповіді для JavaScript.
Для кожного запиту браузер може додати заголовок Origin:
Origin: https://frontend.example.comСервер читає це значення та вирішує, чи дозволяти запит.
Приклад відповіді:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Content-Type: application/jsonУ такому випадку браузер дозволить сторінці з https://frontend.example.com прочитати JSON.
Для загальнодоступного ресурсу можна використати:
Access-Control-Allow-Origin: *Це означає, що ресурс можна читати з будь-якого джерела. Однак така конфігурація несумісна із credentialed-запитами — запитами з cookie, HTTP-авторизацією або TLS-сертифікатами клієнта.
Не кожен кросдоменний запит потребує попередньої перевірки. Браузер може одразу надіслати простий запит (simple request), якщо він відповідає обмеженому набору умов.
Зазвичай простим вважається запит, який:
використовує метод GET, HEAD або POST;
містить лише дозволені прості заголовки;
для Content-Type використовує одне з таких значень:
application/x-www-form-urlencoded;
multipart/form-data;
text/plain.
Наприклад:
const response = await fetch("https://api.example.com/search?q=javascript");
const data = await response.json();
console.log(data);Браузер може одразу надіслати:
GET /search?q=javascript HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.comСервер має додати CORS-заголовок у відповідь:
Access-Control-Allow-Origin: https://frontend.example.comНавіть для простого запиту сервер може отримати сам HTTP-запит. CORS не є механізмом, який забороняє серверу отримувати запити. Він переважно контролює, чи зможе JavaScript прочитати відповідь.
Якщо запит не відповідає правилам простого запиту, браузер спочатку виконує preflight-запит.
Preflight — це запит методом OPTIONS, який перевіряє, чи дозволяє сервер майбутній запит.
Наприклад, код:
const response = await fetch("https://api.example.com/users", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-Request-Id": "abc-123"
},
body: JSON.stringify({
name: "Olena"
})
});може спричинити такий preflight:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type, x-request-idУ preflight браузер повідомляє:
джерело сторінки через Origin;
майбутній метод через Access-Control-Request-Method;
нестандартні заголовки через Access-Control-Request-Headers.
Сервер має відповісти, наприклад:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Request-IdПісля цього браузер може надіслати основний PATCH-запит.
Якщо preflight не отримав потрібних дозволів, основний запит браузер не надсилатиме.
Access-Control-Allow-OriginВизначає дозволене джерело:
Access-Control-Allow-Origin: https://frontend.example.comАбо дозволяє будь-яке джерело:
Access-Control-Allow-Origin: *Сервер не може вказати кілька джерел через кому:
Access-Control-Allow-Origin: https://a.example.com, https://b.example.comЦе некоректний варіант. Якщо дозволено кілька джерел, сервер зазвичай:
читає значення Origin;
перевіряє його за списком дозволених джерел;
повертає саме це значення, якщо воно дозволене.
Access-Control-Allow-MethodsВизначає методи, дозволені для кросдоменних запитів:
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONSЦей заголовок особливо важливий для відповіді на preflight.
Access-Control-Allow-HeadersВизначає заголовки, які JavaScript може передавати в запиті:
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-IdНазви заголовків не залежать від регістру, але їх потрібно вказати у відповіді сервера.
Access-Control-Allow-CredentialsДозволяє надсилати облікові дані:
Access-Control-Allow-Credentials: trueДо облікових даних належать:
cookie;
HTTP-авторизація;
TLS-сертифікат клієнта.
Наприклад:
const response = await fetch("https://api.example.com/profile", {
credentials: "include"
});У такому випадку сервер має повернути:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: trueКомбінація нижче некоректна:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueДля credentialed-запиту потрібно вказати конкретне джерело, а не *.
Access-Control-Expose-HeadersЗа замовчуванням JavaScript може прочитати лише обмежений набір response-заголовків. Якщо сервер хоче відкрити власний заголовок, його потрібно перелічити:
Access-Control-Expose-Headers: X-Total-Count, X-Request-IdТоді код зможе звернутися до нього:
const requestId = response.headers.get("X-Request-Id");
console.log(requestId);Access-Control-Max-AgeВизначає, скільки секунд браузер може кешувати результат preflight:
Access-Control-Max-Age: 600Протягом цього часу браузеру не потрібно повторювати preflight для відповідних запитів.
Нижче наведено повний приклад HTTP-сервера на Node.js без сторонніх бібліотек.
Він:
дозволяє лише http://localhost:3000;
обробляє preflight-запити;
дозволяє GET і POST;
дозволяє заголовки Content-Type та Authorization;
повертає CORS-заголовки для основних відповідей.
const http = require("node:http");
const PORT = 4000;
const allowedOrigin = "http://localhost:3000";
const server = http.createServer((request, response) => {
const origin = request.headers.origin;
if (origin === allowedOrigin) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Vary", "Origin");
response.setHeader("Access-Control-Allow-Credentials", "true");
}
if (request.method === "OPTIONS") {
const requestedMethod = request.headers["access-control-request-method"];
const requestedHeaders = request.headers["access-control-request-headers"];
if (
origin !== allowedOrigin ||
!requestedMethod ||
!["GET", "POST"].includes(requestedMethod)
) {
response.writeHead(403, {
"Content-Type": "text/plain; charset=utf-8"
});
response.end("CORS preflight заборонено");
return;
}
response.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS"
);
response.setHeader(
"Access-Control-Allow-Headers",
requestedHeaders || "Content-Type, Authorization"
);
response.setHeader("Access-Control-Max-Age", "600");
response.writeHead(204);
response.end();
return;
}
if (request.method === "GET" && request.url === "/api/message") {
response.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"X-Request-Id": "request-123"
});
response.end(JSON.stringify({
message: "Вітаю з API",
origin
}));
return;
}
if (request.method === "POST" && request.url === "/api/message") {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => {
let parsedBody;
try {
parsedBody = JSON.parse(body);
} catch {
response.writeHead(400, {
"Content-Type": "application/json; charset=utf-8"
});
response.end(JSON.stringify({
error: "Некоректний JSON"
}));
return;
}
response.writeHead(201, {
"Content-Type": "application/json; charset=utf-8",
"Access-Control-Expose-Headers": "X-Request-Id",
"X-Request-Id": "request-456"
});
response.end(JSON.stringify({
received: parsedBody
}));
});
return;
}
response.writeHead(404, {
"Content-Type": "application/json; charset=utf-8"
});
response.end(JSON.stringify({
error: "Маршрут не знайдено"
}));
});
server.listen(PORT, () => {
console.log(`API працює на http://localhost:${PORT}`);
});Збережіть код у файлі server.js та запустіть:
node server.jsЩоб перевірити сервер із коду, який працює на http://localhost:3000, можна виконати:
async function loadMessage() {
const response = await fetch("http://localhost:4000/api/message");
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const data = await response.json();
console.log(data);
}
async function createMessage() {
const response = await fetch("http://localhost:4000/api/message", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
text: "Повідомлення від клієнта"
})
});
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
const data = await response.json();
console.log(data);
}
loadMessage().catch(console.error);
createMessage().catch(console.error);POST із Content-Type: application/json не є простим запитом, тому перед ним браузер зазвичай виконає OPTIONS.
CORS не замінює налаштування cookie. Для кроссайтових cookie важливі також атрибути cookie, зокрема:
Set-Cookie: session=abc; SameSite=None; SecureНа клієнті потрібно явно дозволити передачу облікових даних:
fetch("https://api.example.com/profile", {
credentials: "include"
});На сервері необхідні:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: trueЯкщо використовується SameSite=None, cookie також має мати атрибут Secure, тобто передаватися через HTTPS.
Запит із заголовком Authorization часто викликає preflight:
const response = await fetch("https://api.example.com/orders", {
headers: {
Authorization: "Bearer token-value"
}
});Сервер має дозволити цей заголовок:
Access-Control-Allow-Headers: AuthorizationCORS не перевіряє правильність токена. Він лише визначає, чи може браузер виконати кросдоменну взаємодію та передати відповідь JavaScript.
Не можна вважати CORS захистом API від сторонніх HTTP-клієнтів.
Запит до API можна виконати за допомогою:
curl;
серверного коду;
мобільного застосунку;
іншого HTTP-клієнта.
Такі клієнти не зобов’язані дотримуватися браузерної політики CORS.
Тому сервер все одно має:
перевіряти автентифікацію;
перевіряти права доступу;
валідовувати вхідні дані;
захищатися від CSRF, якщо використовуються cookie;
не повертати секретні дані лише тому, що джерело дозволене.
CORS контролює доступ браузерного JavaScript до відповіді, а не захищає сам сервер від усіх клієнтів.
Коли CORS налаштовано неправильно, у консолі браузера можна побачити повідомлення на кшталт:
Access to fetch at ... from origin ... has been blocked by CORS policyУ JavaScript це часто виглядає як загальна помилка мережі:
try {
const response = await fetch("https://api.example.com/data");
console.log(await response.json());
} catch (error) {
console.error(error);
}Зазвичай JavaScript не отримує детального пояснення CORS. Точну причину потрібно шукати в:
DevTools;
вкладці Network;
preflight-запиті OPTIONS;
response-заголовках;
серверних логах.
Поширені причини:
сервер не повернув Access-Control-Allow-Origin;
джерело не збігається з дозволеним;
метод відсутній у Access-Control-Allow-Methods;
заголовок відсутній у Access-Control-Allow-Headers;
використано * разом із Access-Control-Allow-Credentials: true;
сервер не обробляє метод OPTIONS;
проксі або балансувальник видаляє CORS-заголовки;
CORS-заголовки є для успішної відповіді, але відсутні для помилки 4xx або 5xx.
Якщо запит не працює, перевіряйте його послідовно:
Відкрийте вкладку Network у DevTools.
Знайдіть запит OPTIONS.
Перевірте його заголовок Origin.
Перевірте Access-Control-Request-Method.
Перевірте Access-Control-Request-Headers.
Перевірте статус відповіді сервера.
Перевірте Access-Control-Allow-Origin.
Перевірте дозволений метод.
Перевірте дозволені заголовки.
Перевірте, чи є CORS-заголовки також у відповіді основного запиту.
Якщо OPTIONS повертає 404, 405 або 500, основний запит, найімовірніше, не буде виконано.
mode: "no-cors"У fetch можна вказати:
fetch("https://example.com/data", {
mode: "no-cors"
});Це не спосіб увімкнути CORS.
У режимі no-cors браузер:
обмежує набір методів і заголовків;
не дозволяє JavaScript прочитати звичайний вміст відповіді;
повертає так звану opaque response.
Тому такий код зазвичай не вирішує проблему:
const response = await fetch(url, {
mode: "no-cors"
});
const data = await response.json();Виклик response.json() для непрозорої відповіді не дасть доступу до даних. Правильне рішення — налаштувати CORS на сервері або виконувати запит через власний серверний проксі.
Якщо сторонній API не підтримує CORS, браузерний клієнт не може напряму прочитати його відповідь. Архітектура може виглядати так:
Браузер → ваш сервер → сторонній APIБраузер звертається до вашого сервера з того самого джерела, а вже серверний код виконує запит до стороннього API. Сервери не обмежені браузерною Same-Origin Policy.
Проксі потрібно проєктувати обережно:
не дозволяти довільні URL без перевірки;
не перетворювати його на open proxy;
обмежувати методи та розмір запитів;
правильно передавати статуси й помилки;
не розкривати приватні токени клієнту.
Access-Control-Allow-Origin: *Це може бути прийнятно для справді публічних ресурсів, але небезпечно для приватних даних. Для приватного API краще використовувати явний список дозволених джерел.
* разом із cookieНекоректна конфігурація:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueПотрібно вказати конкретне джерело:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: trueЯкщо клієнт надсилає:
Authorization: Bearer tokenсервер має явно дозволити Authorization у Access-Control-Allow-Headers.
OPTIONSСервер може коректно працювати з GET і POST, але повертати помилку на OPTIONS. У такому разі браузер заблокує складний запит ще до його виконання.
Якщо API повертає 401 або 500 без CORS-заголовків, браузер може показати клієнту загальну CORS-помилку замість доступної відповіді з конкретним статусом.
Ці адреси мають різні джерела:
http://localhost:3000
http://localhost:4000Для CORS важливий порт, навіть якщо ім’я хоста однакове.
CORS застосовується браузером. Запит із Node.js, curl або Postman може працювати, хоча такий самий запит із браузера буде заблоковано. Це не суперечність, а наслідок різних правил виконання.
Дозволяйте лише потрібні джерела.
Не використовуйте * для приватних API.
Для preflight явно налаштовуйте методи та заголовки.
Повертаючи динамічне значення Origin, додавайте Vary: Origin.
Додавайте CORS-заголовки до відповідей із помилками.
Не вважайте CORS заміною автентифікації та авторизації.
Перевіряйте preflight у вкладці Network.
Не використовуйте mode: "no-cors" як спосіб прочитати заблоковану відповідь.
Для cookie узгоджуйте CORS, credentials і атрибути SameSite.
Обмежуйте список дозволених методів і заголовків найменшим необхідним набором.
Same-Origin Policy обмежує доступ браузерного JavaScript до ресурсів іншого джерела.
CORS дозволяє серверу оголосити правила для кросдоменних запитів.
Заголовок Origin повідомляє сервер, з якого джерела надійшов запит.
Простий запит може бути надісланий одразу, але відповідь усе одно має містити правильний CORS-заголовок.
Складний запит починається з OPTIONS-запиту — preflight.
Access-Control-Allow-Methods і Access-Control-Allow-Headers описують дозволи для preflight.
Access-Control-Allow-Credentials потрібен для cookie та інших облікових даних.
Access-Control-Allow-Origin: * не можна поєднувати з credentialed-запитами.
CORS не захищає API від запитів із не браузерних клієнтів.
Для діагностики потрібно перевіряти OPTIONS, заголовки відповіді та серверні логи.