
Irgendetwas ruft JSON.parse auf, direkt oder über res.json(), und die Konsole sagt eines von
beidem:
SyntaxError: Unexpected end of JSON input
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Fast alle lesen das als Beschwerde über ihr JSON und gehen ihr JSON ansehen. Das JSON ist fast nie das Problem. Meist gibt es überhaupt kein JSON, und der Parser berichtet Ihnen vom ersten Ding, das er nicht verwenden konnte.
Das sind die Meldungen, die Chrome ausgibt. Firefox und Safari formulieren ihre anders; die Ursachen unten sind dieselben, aus welchem Browser die Meldung auch kam.
Was Ihnen jede Meldung tatsächlich sagt
In einer Browser-Konsole durchprobiert, kommt das heraus:
JSON.parse("") // Unexpected end of JSON input
JSON.parse(" ") // Unexpected end of JSON input
JSON.parse("undefined") // "undefined" is not valid JSON
JSON.parse("<html>") // Unexpected token '<', "<html>" is not valid JSON
JSON.parse('{"a":1,') // Expected double-quoted property name in JSON
// at position 7 (line 1 column 8)
In dieser Liste steckt eine Diagnoseregel, die nirgends ausgesprochen wird, und sie ist das Nützlichste auf dieser Seite:
- Eine Meldung mit einer Positionsangabe heißt, dass Sie wirklich JSON haben und es früh endet oder unterwegs ungültig wird. Irgendetwas hat eine echte Nutzlast abgeschnitten.
- Eine Meldung, die einen Ausschnitt zitiert und mit “is not valid JSON” endet, heißt, dass es nie JSON war. Man hat Ihnen etwas völlig anderes gegeben.
- “Unexpected end of JSON input” heißt, dass der Parser das Ende erreicht hat, bevor er einen vollständigen Wert hatte, was in der Praxis einen leeren Body bedeutet. Leerzeichen zählen als leer.
Das sind drei verschiedene Fehler mit drei verschiedenen Lösungen, und der Fehlertext ist das Einzige, was sie unterscheidet.
Ein leerer Body, und die vier Wege dorthin
“Unexpected end of JSON input” ist das, was JSON.parse("") erzeugt, also lautet die Frage,
warum der Body leer war.
Eine Antwort mit 204 No Content ist die ehrliche Variante: Der Server sagt Ihnen, dass da
nichts ist, und .json() darauf aufzurufen ist der Fehler. Eine 200 mit leerem Body ist
dieselbe Lage ohne die Warnung. Ein bereits gelesener Body tut es ebenso: Response-Bodies
sind Streams und lassen sich einmal lesen, also scheitert await res.text() gefolgt von
await res.json() beim zweiten Aufruf mit genau dieser Meldung. Und eine abgebrochene
Anfrage kann Ihnen je nach Abbruchstelle einen teilweisen oder leeren Body hinterlassen.
Eine Webseite, und das ist der häufige Fall
Die andere Meldung ist die, der Sie wirklich begegnen werden, und sie heißt, dass Ihre Anfrage HTML zurückgegeben hat.
So häufig ist sie wegen einer Eigenheit von fetch, über die jeder einmal stolpert: fetch
lehnt bei 4xx oder 5xx nicht ab. Eine 404 ist für das Promise ein vollkommen erfolgreiches
Fetch. Wenn also die URL falsch ist, oder die Sitzung ablief und ein Proxy mit einer
Anmeldeseite antwortete, oder eine Web Application Firewall eine Prüfung ausliefert, oder der
Server eine 500 geworfen hat und Ihr Framework seine eigene Fehlerseite gerendert hat, bekommt
res.json() ein Dokument, das mit <!DOCTYPE html> beginnt, und berichtet das <.
Eine Anmeldeseite ist davon die unangenehmste, weil sie meist als 200 ankommt. Am Statuscode ist nichts falsch. Der Body ist bloß ein Anmeldeformular.
Die Verbesserung, die ein Zeichen vor dem Nutzen aufhört
Diese Meldungen waren früher schlechter. V8s Issue 6551, “JSON.parse errors can be user-friendly
for short strings”, wurde 2017 eröffnet und im Mai 2022 umgesetzt: davor
Unexpected token N in JSON at position 0, danach "NaN" is not valid JSON. Die neue Form
zitiert, was sie bekommen hat, was eine echte Verbesserung ist und der Grund dafür, dass die
Wendung “in JSON at position 0” aus den Browsern weitgehend verschwunden ist und in jedem
Suchergebnis darüber stehen bleibt.
Für dieses Zitat gibt es eine Längengrenze, und sie zu messen lohnt eine Minute, weil sie nirgends dokumentiert ist. Parsen Sie eine Zeichenfolge von zwanzig Zeichen oder weniger, und die Meldung zitiert das Ganze. Ab einundzwanzig zitiert sie die ersten zehn Zeichen und hängt drei Punkte an.
Zählen Sie jetzt die Zeichen in <!DOCTYPE . Es sind genau zehn.
Für die mit Abstand häufigste Ursache dieses Fehlers lautet der hilfreiche neue Ausschnitt also
jedes Mal "<!DOCTYPE ".... Eine 404-Seite, eine 500-Seite, eine Anmeldeumleitung und eine
Firewall-Prüfung erzeugen zeichengenau dieselbe Meldung. Eine echte 404 von unserer eigenen
Seite, ein 3.407 Byte großes HTML-Dokument, erzeugt genau das und nichts weiter.
Die Meldung sieht aus, als zitierte sie den Server für Sie. Für den Fall, in dem Sie am wahrscheinlichsten stecken, zitiert sie den einen Teil der Antwort, den jede HTML-Seite teilt.
Weshalb die Lösung nie im Parser liegt
Prüfen Sie die Antwort, bevor Sie sie parsen, und sagen Sie, was Sie gefunden haben, wenn sie falsch ist:
const res = await fetch(url);
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText} from ${res.url}`);
}
const type = res.headers.get("content-type") || "";
if (!type.includes("application/json")) {
throw new Error(`expected JSON, got ${type || "no content-type"} from ${res.url}`);
}
return res.json();
Beide Prüfungen verdienen ihren Platz. Die erste gibt es, weil fetch bei einem schlechten
Status nicht wirft. Die zweite gibt es, weil eine 200 trotzdem HTML sein kann, was genau der Fall
mit der Anmeldeseite ist, und weil sie einen unlesbaren Parser-Fehler in einen Satz verwandelt,
der Status, Typ und URL nennt.
Und die Fassung, die als Ticket ankommt, ist schlimmer
Alles oben setzt voraus, dass Sie die Antwort sehen können. Die Person, die den Fehler meldet, kann das nicht.
Was ankommt, ist “da steht irgendwas von JSON”, oder im besten Fall ein Foto einer Konsolenzeile
mit is not valid JSON. Diese Zeile trägt nicht den Statuscode, nicht den Content-Type, nicht
die abgerufene URL und nicht den Body. Alle fünf Ursachen oben erzeugen denselben Konsolentext,
und vier der fünf sind ohne die Antwort unsichtbar.
Damit ist das einer der Fehler, bei denen die Konsole allein nicht reicht und der Netzwerk-Reiter das Artefakt ist, auf das es ankommt: die Anfrage, ihr Status, ihr Content-Type und die ersten paar hundert Byte des Rückgelieferten. Den Netzwerk-Reiter zu öffnen ist eine Minute Arbeit für jemanden, der es schon getan hat, und ein anderer Nachmittag für jemanden, der es nicht hat.
Lohnt die Abgrenzung zum Nachbarn, weil Leute nach dem einen suchen und das andere haben:
TypeError: Failed to fetch ist die Anfrage, die nie zustande kommt. Dieser Fehler ist
die Anfrage, die perfekt zustande kommt und das Falsche zurückgibt.
Session Replay
Kostenlose Chrome-Erweiterung. Ein Klick auf die Seite, die Probleme macht, erfasst den Screenshot, die Konsole und das Netzwerkprotokoll und liefert Ihnen einen Link zum Einfügen in das Ticket.
Was davon zu behalten ist
Die Meldung nennt JSON, weil JSON.parse die Stelle ist, an der Ihr Programm anhielt, nicht die,
an der es schiefging. Lesen Sie sie als Aussage über die Antwort: leer, oder kein JSON, oder
abgeschnitten. Und sehen Sie dann die Antwort an, denn der Parser hat Ihnen schon alles gesagt,
was er weiß.