Menu

CORS в Express: как исправить ошибку CORS (с cors и без)

Ошибка CORS значит, что браузер не дал странице прочитать ответ вашего API, потому что ответ не разрешал origin этой страницы. Исправляется это на сервере: отправьте Access-Control-Allow-Origin и отвечайте на preflight-запросы OPTIONS, вручную или через пакет cors.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Ошибка CORS ("No 'Access-Control-Allow-Origin' header is present") значит, что веб-страница с одного origin, например http://localhost:5173, обратилась к вашему API на другом origin, и браузер не дал странице прочитать ответ, потому что в ответе не было сказано, что этот origin разрешён. Исправляется это на сервере: отправляйте Access-Control-Allow-Origin с origin страницы и отвечайте на preflight-запрос браузера OPTIONS списком методов и заголовков, которые вы принимаете. В Express это несколько строк middleware или app.use(cors()) из пакета cors.

В ответе список товаров, а на вкладке Headers видно access-control-allow-origin: https://app.example.com: странице с этого origin можно его читать. Теперь отправьте OPTIONS /api/products/1 с заголовками Origin: https://app.example.com и Access-Control-Request-Method: PUT. Это тот preflight, который браузер отправляет перед PUT, и в ответ приходит 204 с разрешёнными методами и заголовками и без тела.

Что проверяет браузер

Страницам с того же origin ничего не нужно. Для запроса на другой origin браузер читает эти заголовки ответа:

ЗаголовокЗначение
Access-Control-Allow-OriginЕдинственный origin, которому можно читать ответ, или * для любого
Access-Control-Allow-MethodsМетоды, разрешённые в настоящем запросе (только в preflight)
Access-Control-Allow-HeadersЗаголовки запроса, которые может отправить страница, например Authorization (только в preflight)
Access-Control-Allow-Credentialstrue, если странице можно отправлять cookie и читать ответ
Access-Control-Max-AgeСколько секунд браузер может кэшировать ответ на preflight
Vary: OriginСообщает кэшам, что ответ зависит от заголовка запроса Origin

Простой GET или POST в стиле формы идёт прямо на сервер, и браузер проверяет Access-Control-Allow-Origin в ответе. Сервер всё равно выполняет запрос; CORS решает только, увидит ли страница результат. Перед запросом с JSON-телом, заголовком Authorization или методом вроде PUT или DELETE отправляется preflight-запрос OPTIONS, и настоящий запрос уходит, только если ответ на preflight это разрешает. Поэтому CORS middleware должен отвечать на OPTIONS до того, как сработает любой маршрут или проверка авторизации.

Несколько origin и credentials

В Access-Control-Allow-Origin лежит один origin или *, но никогда не список. Чтобы разрешить несколько, сравнивайте заголовок запроса Origin со списком разрешённых и при совпадении возвращайте его обратно:

У первого запроса нет заголовка Origin, как у вызова из curl или со страницы того же origin, поэтому он получает {"origin":null} и никаких заголовков CORS. Отправьте GET /api/me с заголовком Origin: http://localhost:5173, и на вкладке Headers появится этот же origin, а также access-control-allow-credentials: true. Поменяйте заголовок на Origin: https://evil.example, и оба заголовка CORS пропадут: сервер по-прежнему отвечает, но браузер не дал бы странице прочитать ответ. Access-Control-Allow-Credentials: true разрешает странице отправлять cookie (через fetch(url, { credentials: 'include' })), а вместе с * браузеры его отвергают, поэтому для credentials всегда нужен точный origin.

Пакет cors

Пакет cors делает то же самое через опции. Установите его командой npm install cors:

const cors = require('cors');

// Any origin, no cookies: fine for a public read-only API.
app.use(cors());

// Only your frontend, with cookies.
app.use(cors({
    origin: ['https://app.example.com', 'http://localhost:5173'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

// One route only.
app.get('/api/public', cors(), handler);

Без опций cors() отправляет Access-Control-Allow-Origin: * и отвечает на каждый preflight. Используйте * только для публичных данных, которым не нужен вход.

Частые ошибки

CORS добавлен после маршрутов или после middleware авторизации. У preflight-запроса OPTIONS нет заголовка Authorization, поэтому проверка авторизации, стоящая первой, отвечает на него 401, и браузер сообщает об ошибке CORS. Регистрируйте CORS middleware первым.

* вместе с credentials. Браузеры отвергают такой ответ. Возвращайте конкретный origin.

Попытка исправить CORS на фронтенде. Опции запроса вроде mode: 'no-cors' не снимают ограничение; они дают непрозрачный ответ, который нельзя прочитать. Исправить это могут только заголовки сервера.

Как работают app.use() и порядок middleware, читайте в статье Express middleware.

Часто задаваемые вопросы

Почему API работает в Postman или curl, но не в браузере?

CORS соблюдают только браузеры. Postman, curl и другие серверы игнорируют заголовки CORS, поэтому читают ответ в любом случае. Браузер отказывается отдавать ответ скриптам страницы с другого origin, если сервер этот origin не разрешил.

CORS защищает мой сервер?

Он защищает пользователей, а не ваш сервер. Он не даёт вредоносной странице читать ответы вашего API с cookie пользователя. Вызвать ваш API из скрипта или с сервера по-прежнему может кто угодно, так что аутентификация и авторизация остаются вашей задачей.

Что считается другим origin?

Origin это схема, хост и порт вместе. http://localhost:5173 и http://localhost:3000 разные origin, потому что у них разные порты, а https://example.com и https://api.example.com отличаются хостом.

Можно ли обойтись без CORS при разработке?

Да, если сделать запрос same-origin: настройте dev-сервер фронтенда (например, server.proxy в Vite), чтобы он перенаправлял /api на сервер Express. Тогда браузер общается с одним origin, и заголовки CORS не нужны.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

Начать