CORS error: как найти причину и исправить на сервере
Автор: Казачкин Даниил Михайлович · Обновлено
CORS error появляется, когда браузерный JavaScript обращается к другому origin, а ответ сервера не разрешает странице прочитать результат по правилам Fetch. Исправление находится на сервере или reverse proxy: клиент не может выдать себе разрешение заголовком запроса.
Что считается другим origin
Origin состоит из схемы, имени хоста и порта. https://app.example.ru и https://api.example.ru имеют разные хосты; http://localhost:5173 и http://localhost:3000 — разные порты; HTTP и HTTPS — разные схемы. Путь после хоста на origin не влияет.
CORS — браузерный механизм доступа к ответу. Запрос через curl, backend-to-backend HTTP или Postman не доказывает, что браузер сможет прочитать тот же ответ. И наоборот, CORS не является аутентификацией и не запрещает серверу получить запрос: API всё равно обязан проверять пользователя, права и входные данные.
Как проходит простой CORS-запрос
Браузер добавляет Origin, например Origin: https://app.example.ru. Сервер должен вернуть подходящий Access-Control-Allow-Origin. Для публичного ответа без credentials допустим *; для cookies или другого credentialed режима нужен конкретный origin, а не wildcard.
Проверить заголовок можно независимо от frontend-кода:
curl -i 'https://api.example.ru/health' -H 'Origin: https://app.example.ru'Ищите Access-Control-Allow-Origin: https://app.example.ru в фактическом HTTP-ответе, включая ответы 401, 403 и 500. Если успешный ответ содержит заголовок, а ошибка — нет, браузер покажет CORS-проблему и скроет полезное тело именно в аварийной ветке.
Зачем браузер отправляет preflight OPTIONS
Для запроса, который не относится к CORS-safelisted, браузер сначала делает preflight. Например, PATCH, заголовок Authorization или несейфлистовый Content-Type обычно требуют OPTIONS. Предварительный запрос сообщает желаемый метод и заголовки, а сервер отвечает разрешёнными значениями.
curl -i -X OPTIONS 'https://api.example.ru/lessons/42' -H 'Origin: https://app.example.ru' -H 'Access-Control-Request-Method: PATCH' -H 'Access-Control-Request-Headers: authorization,content-type'Успешный preflight должен согласовать origin, метод и запрошенные заголовки. Его ответ не обязан содержать бизнес-данные. Если proxy не пропускает OPTIONS, перенаправляет его на страницу входа или требует bearer token до CORS-обработчика, основной PATCH вообще не будет отправлен браузером.
Корректная настройка CORS в NestJS
Настройте CORS один раз на HTTP-границе приложения и перечислите доверенные frontend-origin. Следующий фрагмент подходит для main.ts NestJS-проекта и разрешает production UI и локальную разработку.
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: ["https://app.example.ru", "http://localhost:5173"],
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 600,
});
await app.listen(3000);
}
void bootstrap();Список должен поступать из проверенной конфигурации окружения, если домены различаются между стендами. Не отражайте любой входной Origin без allowlist: это фактически превращает ограничение в разрешение для произвольного сайта. Если cookies не используются, не включайте credentials автоматически.
Диагностика по слоям
Начните во вкладке Network браузера, а не только с текста Console. Найдите основной запрос и возможный OPTIONS, запишите request URL, Origin, status, redirect и все Access-Control-* заголовки. Затем повторите preflight через curl, чтобы отделить серверную конфигурацию от поведения приложения.
Проверяйте слои по порядку: backend, ingress или Nginx, CDN, затем браузер. CORS-заголовок должен формироваться в одном ответственном месте. Если backend и proxy оба добавляют Access-Control-Allow-Origin, дублированные значения могут сделать ответ недействительным. Redirect на другой origin также требует анализа уже конечного ответа.
Типовые причины:
- разрешён
GET, но не фактическийPATCHилиDELETE; - отсутствует
Authorizationсреди разрешённых заголовков; - production origin отличается схемой, поддоменом или портом;
- с
credentials: trueсервер возвращаетAccess-Control-Allow-Origin: *; - middleware CORS выполняется после auth-проверки и не оформляет отказ;
- preflight получает redirect, 404 или HTML вместо ожидаемых заголовков.
Почему клиентские обходы не исправляют CORS
Добавление Access-Control-Allow-Origin в fetch создаёт обычный пользовательский request header и не разрешает доступ: разрешающий заголовок должен прийти в ответе. Режим mode: "no-cors" возвращает opaque response, содержимое и статус которого JavaScript прочитать не может; это не решение для JSON API.
Расширение, отключающее web security, годится только для изолированного эксперимента и скрывает дефект от разработчика. JSONP и публичный proxy меняют архитектуру и поверхность риска. Для своего API правильный путь — согласовать точный origin на сервере и проверить preflight.
Практика: воспроизведите и устраните отказ
Запустите frontend на http://localhost:5173, а NestJS API — на http://localhost:3000. Сделайте PATCH с Content-Type: application/json и Authorization, сначала не включая CORS. Зафиксируйте в Network отдельный OPTIONS и причину отказа. Затем добавьте точный localhost-origin, методы и заголовки в enableCors, повторите запрос и сравните оба ответа.
После успеха намеренно удалите PATCH из методов, затем Authorization из заголовков. Для каждого случая предскажите, какой заголовок ответа станет причиной отказа. Наконец, повторите запрос через curl с Origin и убедитесь, что проверяется именно серверный контракт.
Что важно запомнить
- Origin определяется схемой, хостом и портом; другой путь не создаёт новый origin.
- Разрешение CORS сообщает сервер в ответе, frontend не может назначить его себе.
- Preflight
OPTIONSпроверяет метод и заголовки до основного несейфлистового запроса. - CORS не заменяет authentication, authorization и CSRF-защиту.
- При credentials нельзя сочетать разрешение origin с wildcard
*.