Пошук уроків, статей та іншого контенту
Налаштуйте CORS для браузерних клієнтів, preflight-запитів і безпечного доступу з інших origin.
CORS (Cross-Origin Resource Sharing) — механізм браузера, який визначає, чи може вебсторінка з одного origin звертатися до API з іншого origin.
origin складається з:
протоколу;
домену;
порту.
Наприклад, це різні origins:
http://localhost:3000;
http://localhost:4000;
https://example.com.
Якщо Next.js API працює на https://api.example.com, а клієнт — на https://app.example.com, браузер виконає CORS-перевірку.
CORS не є механізмом автентифікації. Він лише визначає, чи дозволяти браузеру прочитати відповідь. Запити від серверів, curl або Postman не обмежуються політикою CORS браузера.
Route Handler розміщується у файлі route.js або route.ts всередині каталогу app.
Для CORS зазвичай потрібно:
перевірити заголовок Origin;
додати Access-Control-Allow-Origin до звичайної відповіді;
обробити OPTIONS
додати дозволені методи та заголовки;
не дозволяти невідомі origins.
| Заголовок | Призначення | |---|---| | Access-Control-Allow-Origin | Дозволений origin клієнта | | Access-Control-Allow-Methods | Дозволені HTTP-методи | | Access-Control-Allow-Headers | Заголовки, які клієнт може надсилати | | Access-Control-Allow-Credentials | Дозвіл на cookies та інші credentials | | Access-Control-Max-Age | Час кешування результату preflight | | Vary: Origin | Повідомляє кешам, що відповідь залежить від Origin |
Розглянемо endpoint app/api/notes/route.js. Він дозволяє запити лише від двох клієнтських origins:
const allowedOrigins = new Set([
"http://localhost:3000",
"https://app.example.com",
]);
function isAllowedOrigin(origin) {
return origin !== null && allowedOrigins.has(origin);
}
function createJsonResponse(data, status, origin) {
const headers = new Headers({
"Content-Type": "application/json",
Vary: "Origin",
});
if (isAllowedOrigin(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
}
return new Response(JSON.stringify(data), {
status,
headers,
});
}
function createForbiddenResponse() {
return new Response(
JSON.stringify({ error: "Origin не дозволено" }),
{
status: 403,
headers: {
"Content-Type": "application/json",
},
}
);
}
export async function OPTIONS(request) {
const origin = request.headers.get("origin");
if (!isAllowedOrigin(origin)) {
return createForbiddenResponse();
}
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
Vary: "Origin",
},
});
}
export async function GET(request) {
const origin = request.headers.get("origin");
if (origin && !isAllowedOrigin(origin)) {
return createForbiddenResponse();
}
return createJsonResponse(
{
notes: [
{ id: 1, title: "Перша нотатка" },
{ id: 2, title: "Друга нотатка" },
],
},
200,
origin
);
}
export async function POST(request) {
const origin = request.headers.get("origin");
if (origin && !isAllowedOrigin(origin)) {
return createForbiddenResponse();
}
let body;
try {
body = await request.json();
} catch {
return createJsonResponse(
{ error: "Тіло запиту має бути коректним JSON" },
400,
origin
);
}
if (typeof body.title !== "string" || body.title.trim() === "") {
return createJsonResponse(
{ error: "Поле title є обов'язковим" },
422,
origin
);
}
return createJsonResponse(
{
note: {
id: 3,
title: body.title.trim(),
},
},
201,
origin
);
}У цьому прикладі:
GET повертає список нотаток;
POST приймає JSON;
OPTIONS обробляє preflight;
невідомий Origin отримує статус 403;
Access-Control-Allow-Origin містить конкретний origin, а не загальну маску;
Vary: Origin запобігає використанню кешованої відповіді для іншого origin.
Запит без заголовка Origin, наприклад із сервера або через curl, не відхиляється. Але заголовок Access-Control-Allow-Origin для нього не додається, оскільки він потрібен саме браузеру.
Перед деякими cross-origin-запитами браузер автоматично надсилає OPTIONS. Такий запит називається preflight.
Наприклад, клієнт виконує:
fetch("https://api.example.com/api/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
body: JSON.stringify({
title: "Нова нотатка",
}),
});Через метод POST, заголовок Content-Type: application/json та Authorization браузер спочатку може надіслати:
OPTIONS /api/notes HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-typeRoute Handler повинен відповісти заголовками, які підтверджують дозвіл:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, AuthorizationЯкщо preflight-відповідь не містить потрібних дозволів, браузер не виконає основний POST.
OPTIONSOPTIONS — це окремий HTTP-метод, тому Route Handler повинен експортувати окрему функцію:
export async function OPTIONS(request) {
// Формування відповіді для preflight-запиту
}Обробка OPTIONS не замінює CORS-заголовки на основній відповіді. Заголовок Access-Control-Allow-Origin також потрібно додавати до GET, POST та інших методів.
Не кожен cross-origin-запит запускає preflight.
Браузер може виконати простий запит без попереднього OPTIONS, якщо він відповідає обмеженням CORS. Наприклад, простий GET без нестандартних заголовків часто надсилається одразу.
Навіть у цьому випадку API має додати до відповіді:
Access-Control-Allow-Origin: https://app.example.comІнакше браузер отримає відповідь від сервера, але не дозволить JavaScript-коду прочитати її.
Запити з такими ознаками часто потребують preflight:
методи PUT, PATCH або DELETE;
Content-Type: application/json;
заголовок Authorization;
інші нестандартні заголовки.
Якщо клієнт використовує cookies, потрібно явно дозволити credentials.
На сервері:
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Credentials", "true");На клієнті:
fetch("https://api.example.com/api/notes", {
credentials: "include",
});При використанні credentials не можна встановлювати:
Access-Control-Allow-Origin: *Потрібно вказувати конкретний origin:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueТому для endpoint із cookies краще використовувати allowlist і додавати:
"Access-Control-Allow-Credentials": "true"лише для перевіреного origin.
Не слід без перевірки повертати значення заголовка Origin у Access-Control-Allow-Origin:
// Небезпечно без перевірки
headers.set("Access-Control-Allow-Origin", origin);Такий код фактично дозволяє будь-якому origin.
Замість цього використовуйте список дозволених origins:
const allowedOrigins = new Set([
"http://localhost:3000",
"https://app.example.com",
]);
const origin = request.headers.get("origin");
if (origin && !allowedOrigins.has(origin)) {
return new Response("Forbidden", { status: 403 });
}Для production-оточення список можна отримувати зі змінної середовища:
const allowedOrigins = new Set(
process.env.ALLOWED_ORIGINS
.split(",")
.map((origin) => origin.trim())
);Наприклад:
ALLOWED_ORIGINS=http://localhost:3000,https://app.example.comВажливо, щоб значення origins збігалися повністю:
https://app.example.com і http://app.example.com — різні origins;
https://app.example.com і https://app.example.com/ зазвичай потрібно порівнювати як origin без кінцевого /;
різні порти також створюють різні origins.
Якщо CORS-заголовки додані до POST, але не обробляється OPTIONS, складний запит буде заблоковано до виконання POST.
Access-Control-Allow-HeadersЯкщо клієнт надсилає Content-Type або Authorization, ці заголовки потрібно вказати в preflight-відповіді:
Access-Control-Allow-Headers: Content-Type, AuthorizationНазви заголовків нечутливі до регістру, але їх потрібно включити до переліку.
* разом із credentialsНекоректна конфігурація:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueДля credentials використовуйте конкретний origin.
Такий варіант:
Access-Control-Allow-Origin: *може бути прийнятним для публічного API без cookies та приватних даних, але для внутрішнього або автентифікованого API краще використовувати allowlist.
CORS не захищає endpoint від прямих HTTP-запитів. Будь-хто може викликати API через сервер, curl або інший HTTP-клієнт.
Для захисту API потрібні окремі механізми:
перевірка автентифікації;
авторизація;
валідація вхідних даних.
CORS лише контролює доступ JavaScript-коду в браузері до відповіді.
Vary: OriginЯкщо сервер повертає різні CORS-заголовки залежно від Origin, варто додати:
Vary: OriginЦе повідомляє кешам, що відповідь для одного origin не можна безпечно використовувати для іншого.
На клієнті запит може виглядати так:
const response = await fetch("http://localhost:3001/api/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Нотатка з браузера",
}),
});
const data = await response.json();
console.log(data);Якщо клієнт працює на http://localhost:3000, цей origin має бути у списку allowedOrigins.
У DevTools браузера можна перевірити:
чи був запит OPTIONS;
статус preflight-відповіді;
значення Access-Control-Allow-Origin;
наявність потрібного методу в Access-Control-Allow-Methods;
наявність Content-Type або Authorization у Access-Control-Allow-Headers.
CORS потрібен для браузерних запитів між різними origins.
У Route Handler потрібно додавати Access-Control-Allow-Origin до основних відповідей.
Для складних запитів потрібно реалізувати OPTIONS.
У Access-Control-Allow-Methods вказують дозволені методи.
У Access-Control-Allow-Headers вказують заголовки, які клієнт може надсилати.
Для credentials не можна використовувати Access-Control-Allow-Origin: *.
Найбезпечніший підхід — allowlist конкретних origins.
CORS не замінює автентифікацію, авторизацію та валідацію даних.