
TypeError: Failed to fetch - это браузер сообщает, что вызов fetch() так и не завершился. Не
что он завершился неудачей - что он вообще не дошёл до какого-либо результата.
Это различие важнее всего остального на этой странице, потому что оно отсекает почти всё, на что
думают в первую очередь. 404 такого не вызывает. 500 тоже. Это завершённые запросы: сервер ответил,
fetch() разрешается, а response.ok равно false. Failed to fetch означает, что нет никакого
объекта ответа, который можно было бы рассмотреть, и именно поэтому сообщение говорит вам так
мало.
Chrome, Firefox и Safari формулируют это каждый по-своему - Failed to fetch, NetworkError when
attempting to fetch resource, Load failed - и имеют в виду одно и то же.
Причины, в том порядке, в каком они встречаются на деле
CORS. С большим отрывом самая частая. Запрос ушёл, сервер ответил, а браузер отказался отдавать ответ вашему коду, потому что заголовки этого не разрешали. В консоль выводится отдельное сообщение про политику источников, а fetch отклоняется с общей ошибкой. Если читать только отклонение, вы не узнаете ничего; настоящий ответ - строка над ним.
Соединения вообще не было. Нет сети, сбой DNS, узел недоступен, сервер не слушает. Это та же территория, что и семейство ERR_CONNECTION, и панель сети называет это своим именем.
Что-то его заблокировало. Блокировщик рекламы, расширение для приватности, корпоративный прокси. Запросы ко всему, что похоже на аналитику или трекинг, отклоняются ещё до того, как покинут браузер, и панель сети помечает их как заблокированные. На вашей собственной машине это незаметно, если вы единственный, у кого такого расширения нет.
Смешанное содержимое. Страница по https запрашивает http-адрес. Браузер блокирует это
наотрез.
Запрос отменили. Страница ушла на другой адрес, компонент размонтировался, сработал
AbortController, вкладку закрыли посреди запроса. Обычное дело в одностраничном приложении, и в
системе отслеживания ошибок это выглядит как поток сбоев, которых ни у кого не было.
Плохой URL. Опечатка, относительный путь, который разрешился не туда, переменная окружения,
пустая на момент сборки, из-за чего запрос ушёл на undefined/api/thing.
Failed to fetch dynamically imported module
Отдельная разновидность, заслуживающая своего абзаца, потому что причина не в вашем коде, и решение тоже не в нём.
Приложение, собранное с разделением кода, загружает чанки с хешами в именах файлов. Вы выкатываете новую версию, хеши меняются, старые файлы перестают существовать. Тот, у кого страница уже открыта - вкладка, оставленная на ночь, телефон, который так и не перезагрузил страницу, - запрашивает имя файла, которое теперь отдаёт 404, и получает эту ошибку в тот момент, когда переходит на маршрут, чанк которого ещё не загружен.
Два решения, и они работают вместе. Держите файлы предыдущей сборки какое-то время, вместо того чтобы заменять их, чтобы открытые вкладки продолжали работать. И перехватывайте сбой в маршрутизаторе: если динамический импорт отклонён, предложите перезагрузку, потому что перезагрузка действительно всё исправляет.
Как понять, какая из причин у вас
Консоль вам этого не скажет. Панель сети скажет, и это займёт секунд десять.
Откройте её, воспроизведите сбой и найдите запрос, выделенный красным. Затем прочитайте три колонки:
-
Статус. Пусто или
(failed)означает, что запрос так и не завершился.(blocked:...)называет то, что его заблокировало. - Хост. Ваш собственный домен или чужой. Сбой при обращении к третьей стороне - это другая задача, чем сбой при обращении к вашему собственному API.
- Сообщение в консоли прямо над отклонением. Если в нём упоминается источник или preflight, у вас проблема с CORS, и всё остальное на этой странице - отвлечение.
Если запрос вообще не появляется в панели сети, значит, он не покидал браузер: смешанное содержимое, расширение или URL, который никогда не был правильным.
Session Replay
Бесплатное расширение для Chrome. Один клик на странице, которая ведёт себя не так, захватывает скриншот, консоль и сетевой журнал и отдаёт вам ссылку, которую можно вставить в задачу.
Почему эту ошибку хуже всего получать из вторых рук
Всё сказанное выше предполагает, что вы можете её воспроизвести. Чаще всего не можете, потому что причины этой ошибки как раз те, что живут на чужой машине: чужое расширение, чужая сеть, чужой корпоративный прокси, чужая устаревшая вкладка.
Поэтому она приходит к вам как “страница просто пишет, что что-то пошло не так”, а сообщение из
консоли, которое вам, может быть, процитируют, Failed to fetch, - единственное сообщение на
странице, не несущее никакой информации. Свидетельство, которое всё бы решило, - одна строка в
панели сети, в которую никто не смотрит.
Это и есть довод в пользу того, чтобы захватывать сетевой журнал в момент, когда всё происходит, а не задавать вопросы потом. Ручной способ сделать то же самое - это HAR-файл. Так или иначе, этот класс багов решает строка, а не фраза.
Коротко
- Это значит, что запрос так и не завершился. Ни ответа, ни кода состояния, ничего, что можно рассмотреть.
- 404 или 500 её не вызывают - это успешные обращения с неприятными ответами.
- Сначала посмотрите строку консоли над отклонением. Если в ней упоминается CORS, это и есть ваш ответ.
- Затем проверьте, появляется ли запрос в панели сети вообще. Если нет, что-то в браузере остановило его до того, как он ушёл.
- После выкатки “failed to fetch dynamically imported module” означает, что старая вкладка запрашивает файл, который удалила ваша последняя сборка.