12k
All articles

So beheben Sie „Cannot GET“-Fehler nach dem Deployment einer SPA

Beheben Sie Cannot GET- und SPA-404-Fehler nach dem Deploy mit Server-Rewrites für Nginx, Apache, Netlify, Vercel und S3 CloudFront.

OpenReplay Team
OpenReplay Team
So beheben Sie „Cannot GET“-Fehler nach dem Deployment einer SPA

Ein „Cannot GET /route”- oder 404-Fehler nach dem Deployment einer Single-Page-App ist in der Regel ein Problem der Serverkonfiguration und kein Routing-Bug. Die Lösung besteht darin, den Server so einzurichten, dass er für jeden Anfragepfad, der keiner echten Datei entspricht, index.html zurückgibt.

Das Muster ist bekannt: Der Build geht live, jede Seite funktioniert beim Durchklicken, und dann lädt jemand /dashboard neu oder öffnet einen geteilten Link zu /orders/42 und erhält einen nackten 404. Meist ist der Router völlig in Ordnung, und der Build auch. Der Server wurde nach einer Datei gefragt, die es nicht gibt.

Dieser Artikel erklärt, warum der Fehler nur bei Hard Navigations auftritt, und behandelt anschließend die Lösung sowie die Konfiguration für Nginx, Apache, Netlify, Vercel und S3 hinter CloudFront – inklusive des Nebeneffekts, um den Sie sich danach kümmern müssen.

Die wichtigsten Punkte

  • Ein SPA-404 beim Neuladen entsteht, weil die Anfrage den Server erreicht, dieser nach einer echten Datei unter diesem Pfad sucht und nur index.html im Root-Verzeichnis findet.
  • Lokale Dev-Server verbergen den Fehler, weil sie für nicht zugeordnete Pfade bereits auf index.html zurückfallen.
  • Die Lösung ist ein Rewrite, kein Redirect: index.html wird mit Status 200 ausgeliefert, damit die URL unverändert bleibt und der Router sie auslesen kann.
  • Bei S3 behält der Ansatz über das Error-Document den 404-Status bei; erst eine CloudFront Custom Error Response, die sowohl 403 als auch 404 auf /index.html abbildet, liefert einen 200 zurück.
  • Ein Catch-all-Fallback bedeutet, dass fehlerhafte URLs einen 200 zurückgeben – die App braucht daher ihre eigene Wildcard-Route, die eine Not-Found-Ansicht rendert.

Wann tritt der Cannot-GET-Fehler auf?

Der Fehler zeigt sich ausschließlich bei Hard Navigations: einem Reload der Seite, einer manuell in die Adressleiste eingegebenen URL oder einem geteilten Deep Link, der in einem neuen Tab geöffnet wird. Die Navigation innerhalb der App funktioniert weiterhin, denn sobald die App geladen ist, wechselt der Router die Ansichten vollständig im Browser, ohne den Server zu kontaktieren. Die genaue Meldung variiert je nach Host: Express-basierte Server geben „Cannot GET /route” aus, während statische Hosts ihre 404-Seite zurückliefern.

Genau deshalb übersteht dieser Bug auch die QA. Session Replays einer frisch deployten SPA zeigen den Abbruch bei Hard Navigations – einem Reload oder einem extern geöffneten Link –, niemals beim Klicken innerhalb der App. Tests, die sich nur durch die laufende App klicken, laufen also fehlerfrei durch, während echte Nutzer auf den 404 stoßen.

Warum ist ein SPA-404 beim Neuladen ein Serverproblem?

Ein statischer Server ordnet jeden Anfragepfad einer Datei auf der Festplatte zu. Ein SPA-Build erzeugt eine einzige HTML-Datei, index.html, plus JS- und CSS-Assets. Eine direkte Anfrage an /dashboard findet also keine Datei unter diesem Pfad, und der Server antwortet korrekt mit 404. React Router, Vue Router und SvelteKit als Single-Page-App konfiguriert laufen alle identisch in dieses Problem, denn das Framework ist irrelevant: Die Routen existieren nur in JavaScript, das noch nicht geladen wurde.

In der lokalen Entwicklung tritt der Fehler nie auf, weil die meisten SPA-Dev-Server den Fallback von Haus aus aktiviert haben: Jeder Pfad, der keiner Datei entspricht, wird automatisch mit index.html beantwortet. Ihr lokales Setup hat also stillschweigend genau das getan, was Ihr Produktionsserver nicht tut.

Wie behebt man einen Cannot-GET-Fehler?

Konfigurieren Sie den Server so, dass er für jeden Anfragepfad, der keiner existierenden Datei entspricht, index.html ausliefert. So lädt die App, und ihr Router rendert die Ansicht für diese URL. Das muss ein Rewrite sein, das index.html mit Status 200 zurückgibt, und kein Redirect: Ein Redirect würde die URL in der Adressleiste verändern, und der Router benötigt den ursprünglichen Pfad unverändert.

HostOrt der KonfigurationMechanismus
Nginxserver-Blocktry_files
Apachevhost oder .htaccessFallbackResource
Netlify_redirects oder netlify.tomlRewrite-Regel mit Status 200
Vercelvercel.jsonrewrites-Array
S3 + CloudFrontBucket-Website-Konfiguration + DistributionError Document + Custom Error Response

Wenn Sie den Server wirklich nicht anfassen können, umgeht Hash-basiertes Routing all das, weil das Fragment den Browser nie verlässt. Allerdings verwandelt es jede URL dauerhaft in /#/about – betrachten Sie es daher als letzten Ausweg.

Nginx und Apache

Nginx und Apache drücken den SPA-Fallback jeweils über eine einzige Direktive in der Serverkonfiguration aus. Fügen Sie bei Nginx einen try_files-Fallback zur Root-Location hinzu. Er sucht den Anfragepfad zunächst als Datei, dann als Verzeichnis, und wenn beides nicht existiert, liefert er intern /index.html mit Status 200 aus:

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

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

Bei Apache erledigt eine einzige Direktive aus mod_dir dieselbe Aufgabe. Echte Dateien werden weiterhin als sie selbst ausgeliefert, alles andere landet beim Fallback:

FallbackResource /index.html

Liegt die App unter einem Unterpfad, müssen Sie diesen mit angeben: FallbackResource /app/index.html. Das ältere mod_rewrite-Äquivalent funktioniert in .htaccess weiterhin:

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

Netlify und Vercel

Bei Netlify wird eine Redirect-Regel mit Status 200 zu einem Rewrite: Der Browser zeigt weiterhin den vom Besucher angeforderten Pfad an, und der Inhalt von index.html kommt in der Response zurück. Legen Sie entweder eine einzeilige _redirects-Datei an:

/* /index.html 200

oder das Äquivalent in netlify.toml:

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

Die _redirects-Datei muss im Publish-Verzeichnis landen – stellen Sie also sicher, dass Ihr Build sie in den Output-Ordner kopiert; netlify.toml liegt hingegen im Repo-Root. Eine Splat-Regel übernimmt keinen Pfad, hinter dem eine echte Datei liegt, JS- und CSS-Assets werden also weiterhin geladen.

Fügen Sie bei Vercel einen rewrites-Eintrag in vercel.json ein:

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

Bevorzugen Sie das explizite Ziel /index.html gegenüber /: Beide verweisen bei Vercel auf dieselbe Datei, aber die explizite Form benennt, was tatsächlich ausgeliefert wird, und lässt sich als mentales Modell auf jeden anderen Host übertragen. Eine Ausnahme: Ist cleanUrls: true gesetzt, darf das Ziel die Endung .html nicht enthalten, und Vercel ordnet index.html dem Site-Root zu – setzen Sie das Ziel dann auf /.

S3 und CloudFront

Einen SPA-404 auf S3 zu beheben erfordert zwei Konfigurationsschritte, denn die Bucket-Einstellung allein behält den Fehlerstatus bei. Setzen Sie beim S3 Static Website Hosting index.html sowohl als Index-Dokument als auch als Error-Dokument:

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

Damit wird für unbekannte Pfade die App ausgeliefert, der Fehlerstatus bleibt jedoch erhalten: Der Browser erhält index.html mit einem 404-Code. Um einen 200 zurückzugeben, fügen Sie CloudFront Custom Error Responses hinzu, die sowohl 403 als auch 404 auf /index.html mit Response-Code 200 abbilden. Die 403-Zuordnung ist wichtig, weil eine Distribution, die den S3-REST-Endpoint als Origin nutzt, für nicht existierende Keys 403 Access Denied erhält und nicht 404. In Terraform-Syntax:

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"
}

Eine Fußangel: Custom Error Responses gelten distributionsweit. Wenn Sie also /api/* über dieselbe Distribution proxyen, kommen API-403er und -404er ebenfalls als index.html zurück.

Der Preis: Ihre echten 404er verschwinden

Ein Catch-all-Fallback hat einen Preis: Tatsächlich falsche URLs geben jetzt index.html mit Status 200 zurück statt eines echten 404. Der Server kann /orders/42 nicht mehr von /ordersss/42 unterscheiden, weshalb die App ihre eigene Wildcard-Route definieren muss, die eine Not-Found-Ansicht rendert. Jeder Router hat dafür eine eigene Schreibweise; in React Router sieht das so aus:

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

Beachten Sie: Das ist ein clientseitig gerenderter 404 – der HTTP-Status bleibt 200. Das ist relevant, wenn Sie darauf Wert legen, wie Crawler diese Seiten einordnen.

Fazit

Der 404 beim Neuladen ist genau das, was statische Server tun, und die Lösung ist eine einzige Regel im Dialekt Ihres Hosts: Jeden Pfad, der keine Datei ist, mit Status 200 auf index.html umschreiben. Fügen Sie das Snippet für Ihren Host hinzu, deployen Sie neu, laden Sie eine tiefe Route hart neu, um es zu bestätigen, und ergänzen Sie dann die Wildcard-Not-Found-Route, damit fehlerhafte URLs den Nutzern weiterhin signalisieren, dass sie sich verlaufen haben.

FAQs

Funktioniert der SPA-Fallback-Fix auch bei GitHub Pages?

Nein. GitHub Pages unterstützt keine serverseitigen Rewrites, es gibt also keine Möglichkeit, eine index.html-Fallback-Regel zu konfigurieren. Der übliche Workaround ist eine eigene 404.html-Seite mit einem Skript, das auf index.html weiterleitet und dabei den angeforderten Pfad bewahrt, den der Router nach dem Laden wiederherstellt. GitHub liefert diese Seite dennoch mit einem 404-Status aus. Die andere Option ist Hash-basiertes Routing, bei dem die Route den Server nie erreicht.

Haben serverseitig gerenderte Frameworks wie Next.js oder Nuxt dieses Problem?

Nicht, wenn sie ihren eigenen Server betreiben. Ein serverseitig gerendertes Framework verarbeitet jede Route auf dem Server, ein Reload oder Deep Link liefert also direkt gerendertes HTML zurück. Das Problem des 404 beim Neuladen betrifft nur statische Single-Page-Builds, in denen Routen ausschließlich in clientseitigem JavaScript existieren. Eine statisch exportierte App aus einem dieser Frameworks kann trotzdem darauf stoßen, wenn für eine angeforderte Route keine vorgerenderte HTML-Datei auf der Festplatte liegt.

Bricht das Umschreiben jedes Pfads auf index.html meine JS- und CSS-Assets?

Nein. Jeder Mechanismus sucht zuerst nach einer echten Datei, bevor er auf den Fallback zurückgreift: Nginx try_files probiert zunächst die Request-URI, Apache FallbackResource lässt Anfragen nach echten Dateien unangetastet, und ein Netlify-Splat-Rewrite übernimmt keinen existierenden Pfad, sofern Sie es nicht mit 200! erzwingen. Wenn Assets nach dem Hinzufügen des Fallbacks weiterhin fehlschlagen, liegt die Ursache meist in relativen Asset-Pfaden, die unter einer verschachtelten Route aufgelöst werden: Der Browser fordert sie dann aus dem falschen Verzeichnis an und erhält stattdessen index.html.

Schadet es der SEO, index.html mit Status 200 auszuliefern?

Das kann sein. Wenn eine nicht existierende URL einen 200-Status mit Not-Found-Inhalt zurückgibt, kann Google sie als 'Soft 404' einordnen und aus dem Index entfernen, weil der Statuscode echte Seiten nicht mehr von fehlerhaften URLs unterscheidet. Wenn die Suchindexierung für Ihre Routen wichtig ist, stellen Pre-Rendering oder serverseitiges Rendering korrekte Statuscodes pro Route wieder her. Bei Apps hinter einem Login sehen Crawler die Routen ohnehin nie, der Kompromiss ist dort also unerheblich.

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.