Ошибка 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-Credentials | true, если странице можно отправлять 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 не нужны.