12k
All articles

Comment corriger les erreurs « Cannot GET » après le déploiement d'une SPA

Corrigez les erreurs Cannot GET et 404 des SPA après déploiement avec des réécritures serveur pour Nginx, Apache, Netlify, Vercel et S3 CloudFront.

OpenReplay Team
OpenReplay Team
Comment corriger les erreurs « Cannot GET » après le déploiement d'une SPA

Une erreur « Cannot GET /route » ou une erreur 404 après le déploiement d’une application monopage relève généralement d’un problème de configuration serveur plutôt que d’un bug de routage. La solution consiste à faire en sorte que le serveur renvoie index.html pour tout chemin de requête ne correspondant pas à un fichier réel.

Le scénario est bien connu. Le build est livré, chaque page fonctionne lorsqu’on navigue en cliquant, puis quelqu’un rafraîchit /dashboard, ou ouvre un lien partagé vers /orders/42, et se retrouve face à un 404 brut. En général, le routeur n’est pas en cause, et le build non plus. On a simplement demandé au serveur un fichier qui n’existe pas.

Cet article explique pourquoi l’erreur n’apparaît que lors des navigations complètes, puis présente la solution et la configuration pour Nginx, Apache, Netlify, Vercel et S3 derrière CloudFront, ainsi que l’effet secondaire à gérer ensuite.

Points clés à retenir

  • Un 404 sur une SPA lors d’un rafraîchissement se produit parce que la requête atteint le serveur, qui cherche un fichier réel à ce chemin et ne trouve qu’index.html à la racine.
  • Les serveurs de développement local masquent le problème car ils appliquent déjà un repli vers index.html pour les chemins non correspondants.
  • La solution est une réécriture (rewrite), pas une redirection : servir index.html avec un statut 200 pour que l’URL reste intacte et lisible par le routeur.
  • Sur S3, l’approche par document d’erreur conserve le statut 404 ; c’est une réponse d’erreur personnalisée CloudFront mappant à la fois 403 et 404 vers /index.html qui permet de renvoyer un 200.
  • Un repli fourre-tout implique que les mauvaises URL renvoient un 200 : l’application doit donc définir sa propre route joker affichant une vue « page non trouvée ».

Quand l’erreur « Cannot GET » apparaît-elle ?

L’erreur ne se manifeste que lors des navigations complètes : rafraîchissement de page, URL saisie dans la barre d’adresse, ou lien profond partagé ouvert dans un nouvel onglet. La navigation interne à l’application continue de fonctionner, car une fois l’application chargée, le routeur change de vue entièrement dans le navigateur, sans contacter le serveur. Le message exact varie selon l’hébergeur : les serveurs basés sur Express affichent « Cannot GET /route », tandis que les hébergeurs statiques renvoient leur propre page 404.

C’est aussi pour cette raison que le bug passe à travers les tests QA. Les rejeux de session d’une SPA fraîchement déployée montrent la rupture lors des navigations complètes — un rafraîchissement ou un lien ouvert depuis l’extérieur — jamais lors des clics au sein de l’application. Des tests qui se contentent de cliquer dans l’application en cours d’exécution passent donc sans encombre, alors que les vrais utilisateurs se heurtent au 404.

Pourquoi un 404 sur une SPA lors d’un rafraîchissement est-il un problème serveur ?

Un serveur statique associe chaque chemin de requête à un fichier sur le disque. Un build de SPA produit un seul fichier HTML, index.html, plus des ressources JS et CSS. Une requête directe vers /dashboard ne trouve donc aucun fichier à ce chemin, et le serveur répond correctement par un 404. React Router, Vue Router et SvelteKit configuré en application monopage rencontrent tous exactement le même problème, car le framework n’a aucune importance : les routes n’existent que dans du JavaScript qui n’est pas encore chargé.

L’erreur n’apparaît jamais en développement local, car la plupart des serveurs de développement pour SPA activent le repli par défaut : tout chemin ne correspondant pas à un fichier reçoit automatiquement index.html. Votre configuration locale faisait discrètement ce que votre serveur de production ne fait pas.

Quelle est la solution à une erreur « Cannot GET » ?

Configurez le serveur pour servir index.html pour tout chemin de requête ne correspondant pas à un fichier existant, afin que l’application se charge et que son routeur affiche la vue correspondant à cette URL. Il doit s’agir d’une réécriture renvoyant index.html avec un statut 200, et non d’une redirection : une redirection modifierait l’URL dans la barre d’adresse, alors que le routeur a besoin du chemin d’origine intact.

HébergeurEmplacement de la configurationMécanisme
Nginxbloc servertry_files
Apachevhost ou .htaccessFallbackResource
Netlify_redirects ou netlify.tomlrègle de réécriture avec statut 200
Vercelvercel.jsontableau rewrites
S3 + CloudFrontconfig website du bucket + distributiondocument d’erreur + réponse d’erreur personnalisée

Si vous n’avez véritablement aucun accès au serveur, le routage par hash contourne tout cela puisque le fragment ne quitte jamais le navigateur, mais il transforme définitivement chaque URL en /#/about : à considérer comme un dernier recours.

Nginx et Apache

Nginx et Apache expriment chacun le repli SPA sous la forme d’une seule directive dans la configuration du serveur. Pour Nginx, ajoutez un repli try_files à l’emplacement racine. Il cherche le chemin de requête comme fichier, puis comme répertoire, et lorsqu’aucun des deux n’aboutit, il sert /index.html en interne avec un statut 200 :

server {
  listen 80;
  root /var/www/app/dist;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

Pour Apache, une seule directive issue de mod_dir remplit le même rôle. Les fichiers réels sont toujours servis tels quels, et tout le reste retombe sur le repli :

FallbackResource /index.html

Si l’application est hébergée sous un sous-chemin, incluez-le : FallbackResource /app/index.html. L’ancien équivalent avec mod_rewrite fonctionne toujours dans .htaccess :

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]

Netlify et Vercel

Sur Netlify, une règle de redirection avec un statut 200 devient une réécriture : le navigateur continue d’afficher le chemin demandé par le visiteur, et le contenu d’index.html est renvoyé dans la réponse. Ajoutez soit un fichier _redirects d’une seule ligne :

/* /index.html 200

soit son équivalent dans netlify.toml :

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Le fichier _redirects doit se retrouver dans le répertoire de publication : assurez-vous donc que votre build le copie dans le dossier de sortie ; netlify.toml, lui, se place à la racine du dépôt. Une règle « splat » ne prendra pas le pas sur un chemin derrière lequel se trouve un fichier réel, si bien que les ressources JS et CSS continuent de se charger.

Pour Vercel, ajoutez une entrée rewrites dans vercel.json :

{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

Préférez la destination explicite /index.html à / : les deux pointent vers le même fichier sur Vercel, mais la forme explicite indique ce qui est réellement servi et se transpose comme modèle mental à tous les autres hébergeurs. Une exception : avec cleanUrls: true, la destination ne peut pas porter l’extension .html, et Vercel associe index.html à la racine du site ; définissez donc la destination à /.

S3 et CloudFront

Corriger un 404 de SPA sur S3 requiert deux éléments de configuration, car le paramètre du bucket à lui seul conserve le statut d’erreur. Dans l’hébergement de site statique S3, définissez index.html à la fois comme document d’index et comme document d’erreur :

aws s3 website s3://your-bucket \
  --index-document index.html \
  --error-document index.html

Cela sert l’application pour les chemins inconnus, mais conserve le statut d’erreur : le navigateur reçoit index.html avec un code 404. Pour renvoyer un 200, ajoutez des réponses d’erreur personnalisées CloudFront mappant à la fois 403 et 404 vers /index.html avec un code de réponse 200. Le mappage du 403 est important, car une distribution utilisant le point de terminaison REST S3 comme origine reçoit un 403 Access Denied, et non un 404, pour les clés inexistantes. En termes Terraform :

custom_error_response {
  error_code         = 403
  response_code      = 200
  response_page_path = "/index.html"
}

custom_error_response {
  error_code         = 404
  response_code      = 200
  response_page_path = "/index.html"
}

Un piège à connaître : les réponses d’erreur personnalisées s’appliquent à l’échelle de toute la distribution. Si vous faites passer /api/* par la même distribution, les 403 et 404 de l’API renverront eux aussi index.html.

Le revers de la médaille : vos vrais 404 disparaissent

Un repli fourre-tout a un coût : les URL réellement erronées renvoient désormais index.html avec un statut 200 au lieu d’un véritable 404. Le serveur ne peut plus distinguer /orders/42 de /ordersss/42 : l’application doit donc définir sa propre route joker affichant une vue « page non trouvée ». Chaque routeur a sa syntaxe pour cela ; avec React Router, cela ressemble à :

<Route path="*" element={<NotFound />} />

Notez qu’il s’agit d’un 404 rendu côté client : le statut HTTP reste 200, ce qui a son importance si la façon dont les robots d’indexation classent ces pages vous concerne.

Conclusion

Le 404 au rafraîchissement n’est rien d’autre que le serveur faisant exactement ce que font les serveurs statiques, et la solution tient en une seule règle exprimée dans le dialecte de votre hébergeur : réécrire tout chemin ne correspondant pas à un fichier vers index.html avec un statut 200. Ajoutez l’extrait correspondant à votre hébergeur, redéployez, rafraîchissez complètement une route profonde pour vérifier, puis ajoutez la route joker « page non trouvée » afin que les mauvaises URL indiquent toujours aux utilisateurs qu’ils se sont égarés.

FAQ

La solution de repli SPA fonctionne-t-elle sur GitHub Pages ?

Non. GitHub Pages ne prend pas en charge les réécritures côté serveur : il n'existe donc aucun moyen de configurer une règle de repli vers index.html. La solution de contournement standard consiste à créer une page 404.html personnalisée contenant un script qui redirige vers index.html en préservant le chemin demandé, que le routeur restaure ensuite après le chargement. GitHub sert malgré tout cette page avec un statut 404. L'autre option est le routage par hash, qui n'envoie jamais la route au serveur.

Les frameworks avec rendu côté serveur comme Next.js ou Nuxt rencontrent-ils ce problème ?

Pas lorsqu'ils exécutent leur propre serveur. Un framework à rendu côté serveur traite chaque route sur le serveur : un rafraîchissement ou un lien profond renvoie donc directement du HTML rendu. Le problème du 404 au rafraîchissement n'affecte que les builds monopage statiques, où les routes n'existent que dans du JavaScript côté client. Une application exportée statiquement depuis l'un de ces frameworks peut malgré tout y être confrontée lorsqu'une route demandée n'a pas de fichier HTML pré-rendu sur le disque.

Réécrire tous les chemins vers index.html va-t-il casser mes ressources JS et CSS ?

Non. Chaque mécanisme cherche d'abord un fichier réel avant d'appliquer le repli : try_files de Nginx essaie d'abord l'URI de la requête, FallbackResource d'Apache laisse intactes les requêtes vers des fichiers réels, et une réécriture splat sur Netlify ne prendra pas le pas sur un chemin existant, sauf si vous la forcez avec 200!. Si les ressources échouent toujours après l'ajout du repli, la cause habituelle est l'utilisation de chemins d'accès relatifs qui se résolvent sous une route imbriquée : le navigateur les demande alors depuis le mauvais répertoire et reçoit index.html à la place.

Servir index.html avec un statut 200 nuit-il au SEO ?

C'est possible. Lorsqu'une URL inexistante renvoie un statut 200 avec un contenu de type « page non trouvée », Google peut la classer comme un « soft 404 » et la retirer de l'index, car le code de statut ne distingue plus les vraies pages des mauvaises URL. Si l'indexation dans les moteurs de recherche compte pour vos routes, le pré-rendu ou le rendu côté serveur rétablit des codes de statut corrects route par route. Pour les applications derrière une authentification, les robots ne voient jamais les routes : le compromis est alors sans objet.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.