Сообщение выглядит примерно так, и важна в нём вторая половина:

Access to fetch at 'https://api.example.com/orders' from origin
'https://app.example.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Читайте его как фразу о том, кто отказал. Сервер ответил. Ваш запрос ушёл из браузера, дошёл до другой машины и вернулся с ответом. А браузер после этого отказался отдавать этот ответ вашему JavaScript, потому что в ответе не было сказано, что вашему источнику разрешено его читать.

Один этот факт отсекает почти всё, что пробуют в первую очередь. В сети ничего не сломано. Сервер не лежит. Никакой межсетевой экран не мешает. Ответ лежит в браузере, и браузер его не отдаст.

Зачем браузер вообще это делает

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

Правило поэтому такое: JavaScript может отправить запрос на чужой источник, но прочитать ответ может только тогда, когда отвечающий сервер говорит, что этому источнику это разрешено. Разрешение должно прийти от того сервера, к которому обращаются, потому что только он знает, кому положено это читать.

Два следствия, которые удивляют:

  • Это касается только JavaScript в браузере. Тот же запрос из curl, из Postman или из вашего собственного бэкенда работает, потому что ни один из них не браузер, оберегающий чью-то сессию. Запрос, который проходит в Postman и падает на странице, не доказывает, что сервер сломан.
  • Отключение проверки в браузере ничего не исправляет. Флаг или расширение, выключающее проверку, убирает ошибку на одной машине, а каждый посетитель продолжает её получать. Это способ подтвердить диагноз, а не исправить, и работать в таком браузере каждый день по-настоящему плохая идея.

Как прочитать, какое правило не сошлось

Конец сообщения называет правило, и таких правил вам встретится три.

No ‘Access-Control-Allow-Origin’ header is present. (Заголовка ‘Access-Control-Allow-Origin’ нет.) Простой случай. Сервер не прислал ничего о разрешениях, значит, браузер считает, что их нет.

Response to preflight request doesn’t pass access control check. (Ответ на предварительный запрос не проходит проверку контроля доступа.) Сбой случился ещё до того, как ваш запрос был отправлен. Всё, что выходит за рамки простого запроса, будь то PUT или DELETE, свой заголовок вроде Authorization или тип содержимого JSON, заставляет браузер сначала отправить запрос OPTIONS и спросить, разрешён ли настоящий. Если этот OPTIONS вернёт 404, 500, редирект или 200 без нужных заголовков, настоящего запроса не будет вовсе.

*The value of ‘Access-Control-Allow-Origin’ must not be the wildcard ‘’ when credentials mode is ‘include’.** (Значение не должно быть подстановочным знаком ‘*’, когда отправляются учётные данные.) Вы отправляете куки, а * для этого недостаточно точен. Сервер обязан назвать ваш источник в точности и добавить Access-Control-Allow-Credentials: true.

Куда вносить исправление

На сервер, которому принадлежит ресурс, во всех случаях. Не в вашу страницу, не в браузер и не в прокси, который вы поставили перед своим собственным фронтендом.

Если это ваш API, значит, нужно возвращать Access-Control-Allow-Origin с тем источником, который вы хотите разрешить, а для предварительных запросов отвечать на OPTIONS разрешёнными методами и заголовками и статусом 2xx. Если это чужой API и он не допускает обращений из браузера, значит, обращения из браузера не предусмотрены: вызывайте его из своего бэкенда, а страница пусть говорит с ним.

Session Replay

Бесплатное расширение для Chrome. Один клик на странице, которая ведёт себя не так, захватывает скриншот, консоль и сетевой журнал и отдаёт вам ссылку, которую можно вставить в задачу.

Установить расширение

На localhost, где с этим встречается большинство

http://localhost:3000 и http://localhost:8080 являются разными источниками, поэтому фронтенд на одном порту, обращающийся к API на другом, делает запрос к чужому источнику и получает всю процедуру целиком.

В разработке правильный ответ даёт прокси: ваш дев-сервер перенаправляет /api на API, браузер видит один источник, и вопрос вообще не встаёт. Во всех современных фронтенд-инструментах это уже есть, и стоит это одной строки настройки. Неправильный ответ состоит в том, чтобы запускать Chrome с отключённой веб-безопасностью, потому что тогда код работает только у тех, кто сделал то же самое.

Что видит ваш код

Ничего полезного, и именно поэтому такую ошибку так путано искать.

Заблокированный ответ не приходит как ошибка, которую можно рассмотреть. fetch() отклоняется с общей ошибкой TypeError: Failed to fetch, без статуса и без тела, потому что дать странице прочитать, почему её заблокировали, значило бы выдать ровно ту информацию, ради защиты которой правило и существует.

Так что сообщение о CORS в консоли не является ошибкой, которую поймал ваш код. Это браузер сообщает вам, разработчику, что он сделал. Ваш код не может его увидеть, не может его записать в лог и не может о нём сообщить.

Это стоит помнить, когда о проблеме сообщает кто-то другой. Пользователь, попавший на сбой CORS, видит функцию, которая молча ничего не делает, ваша система отслеживания ошибок записывает Failed to fetch без подробностей, а фраза, которая всё бы объяснила, была напечатана в консоли, которую никто не сохранил.

Как найти это в отчёте

Два места, и нужны оба.

В консоли лежит сообщение о CORS с источником, URL и правилом, которое не сошлось. Панель сети показывает сам запрос, а при сбое предварительной проверки ещё и запрос OPTIONS, стоящий над настоящим, со своим собственным статусом. Откройте его заголовки ответа: то, чего там нет, и есть весь диагноз.

Если запрос вообще не появляется в панели сети, браузер отказал ещё до отправки, а это значит смешанное содержимое или расширение, а не CORS.

Коротко

  • Сервер ответил; браузер не дал вашему коду прочитать ответ.
  • Это касается только JavaScript в браузере, поэтому curl и Postman ничего не доказывают.
  • Конец сообщения называет правило, которое не сошлось. Читайте его, а не начало.
  • Сбои предварительной проверки происходят до вашего запроса, на OPTIONS, который браузер отправил за вас.
  • Исправлять нужно на отвечающем сервере. Отключение проверки у себя даёт диагноз, а не исправление.
  • На localhost используйте прокси в дев-сервере, чтобы источник был только один.