
TypeError: Failed to fetch, c’est le navigateur qui dit qu’un appel fetch() n’est jamais allé à
son terme. Pas qu’il a échoué - qu’il n’est jamais allé assez loin pour avoir le moindre résultat.
Cette distinction compte plus que tout le reste de cette page, parce qu’elle écarte l’essentiel de
ce que l’on soupçonne d’abord. Un 404 ne produit pas cela. Un 500 non plus. Ce sont des requêtes
menées à terme : le serveur a répondu, fetch() se résout, et response.ok vaut false. Failed to
fetch veut dire qu’il n’y a aucun objet de réponse à inspecter, et c’est exactement pour cela que
le message vous apprend si peu.
Chrome, Firefox et Safari le formulent chacun à leur manière - Failed to fetch, NetworkError when
attempting to fetch resource, Load failed - et veulent dire la même chose.
Les causes, dans l’ordre où elles se produisent vraiment
CORS. De loin la plus fréquente. La requête est partie, le serveur a répondu, et le navigateur a refusé de remettre la réponse à votre code parce que les en-têtes ne l’autorisaient pas. La console affiche un message distinct sur la politique d’origine, et le fetch est rejeté avec l’erreur générique. Si vous ne lisez que le rejet, vous n’apprenez rien ; la ligne au-dessus est la vraie réponse.
La connexion n’a jamais eu lieu. Hors ligne, échec DNS, hôte injoignable, serveur qui n’écoute pas. C’est le même terrain que la famille ERR_CONNECTION, et le panneau réseau la nomme.
Quelque chose l’a bloquée. Un bloqueur de publicités, une extension de confidentialité, un proxy d’entreprise. Les requêtes vers tout ce qui ressemble à de l’analytique ou du pistage sont refusées avant de quitter le navigateur, et le panneau réseau les marque comme bloquées. Celle-ci est invisible sur votre propre machine si vous êtes le seul à ne pas avoir cette extension.
Contenu mixte. Une page https qui demande une URL http. Le navigateur bloque purement et
simplement.
La requête a été annulée. La page a navigué ailleurs, un composant a été démonté, un
AbortController s’est déclenché, l’onglet a été fermé en pleine requête. Banal dans une
application monopage, et cela apparaît dans le suivi des erreurs comme un flot d’échecs qui ne sont
arrivés à personne.
Une mauvaise URL. Une faute de frappe, un chemin relatif qui résout là où il ne fallait pas, une
variable d’environnement vide au moment du build, si bien que la requête est partie vers
undefined/api/thing.
Failed to fetch dynamically imported module
Une variante précise qui mérite son propre paragraphe, parce que la cause n’est pas dans votre code et le correctif non plus.
Une application construite avec découpage du code charge des fragments dont les noms de fichiers portent une empreinte. Vous déployez, les empreintes changent, les anciens fichiers cessent d’exister. Quiconque avait déjà la page ouverte - un onglet laissé ouvert toute la nuit, un téléphone qui n’a jamais rechargé - demande un nom de fichier qui est désormais un 404, et reçoit cette erreur dès qu’il navigue vers une route dont le fragment n’est pas encore chargé.
Deux correctifs, et ils vont ensemble. Gardez un moment les ressources du build précédent au lieu de les remplacer, pour que les onglets ouverts continuent de fonctionner. Et rattrapez l’échec au niveau du routeur : si un import dynamique est rejeté, proposez un rechargement, parce qu’un rechargement règle vraiment le problème.
Comment savoir laquelle vous avez
La console ne vous le dira pas. Le panneau réseau, si, et cela prend une dizaine de secondes.
Ouvrez-le, reproduisez l’échec et trouvez la requête en rouge. Puis lisez trois colonnes :
-
Statut. Vide ou
(failed)signifie que la requête n’est jamais allée à son terme.(blocked:...)nomme ce qui l’a bloquée. - L’hôte. Votre propre domaine, ou celui de quelqu’un d’autre. Un échec contre un tiers est un problème différent d’un échec contre votre propre API.
- Le message de console juste au-dessus du rejet. S’il parle d’une origine ou d’un preflight, vous avez un problème de CORS et tout le reste de cette page est une distraction.
Si la requête n’apparaît pas du tout dans le panneau réseau, c’est qu’elle n’a jamais quitté le navigateur : contenu mixte, une extension, ou une URL qui n’a jamais été valide.
Session Replay
Extension Chrome gratuite. Un clic sur la page qui se comporte mal capture la copie d'écran, la console et le journal réseau, et vous rend un lien à coller dans le ticket.
Pourquoi c’est la pire erreur à recevoir de seconde main
Tout ce qui précède suppose que vous pouvez la reproduire. La plupart du temps vous ne le pouvez pas, parce que les causes qui produisent cette erreur sont celles qui vivent sur la machine de quelqu’un d’autre : son extension, son réseau, son proxy d’entreprise, son onglet périmé.
Elle arrive donc sous la forme « la page dit juste que quelque chose s’est mal passé », et le
message de console qu’on vous citera peut-être, Failed to fetch, est le seul message de la page
qui ne porte aucune information. La preuve qui trancherait est une ligne dans un panneau réseau que
personne ne regarde.
C’est l’argument pour capturer le journal réseau au moment où cela se produit plutôt que de poser des questions après coup. Un fichier HAR est la façon manuelle de le faire. Dans un cas comme dans l’autre, ce qui tranche cette catégorie de bogue, c’est la ligne, pas la phrase.
En bref
- Cela veut dire que la requête n’est jamais allée à son terme. Pas de réponse, pas de code de statut, rien à inspecter.
- Un 404 ou un 500 ne la provoquent pas - ce sont des allers-retours réussis avec des réponses fâcheuses.
- Regardez d’abord la ligne de console au-dessus du rejet. Si elle parle de CORS, vous avez votre réponse.
- Vérifiez ensuite si la requête apparaît seulement dans le panneau réseau. Sinon, quelque chose dans le navigateur l’a arrêtée avant qu’elle ne parte.
- Après un déploiement, « failed to fetch dynamically imported module » veut dire qu’un vieil onglet demande un fichier que votre dernier build a supprimé.