
Quelque chose appelle JSON.parse, directement ou via res.json(), et la console affiche l’un de
ces deux messages :
SyntaxError: Unexpected end of JSON input
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Presque tout le monde les lit comme une plainte au sujet de son JSON et va regarder son JSON. Le JSON n’est presque jamais le problème. En général il n’y a pas de JSON du tout, et l’analyseur vous parle de la première chose qu’il n’a pas pu utiliser.
Ce sont les messages que Chrome affiche. Firefox et Safari formulent les leurs autrement ; les causes ci-dessous sont les mêmes, quel que soit le navigateur d’où vient le signalement.
Ce que chaque message vous dit réellement
Essayé dans une console de navigateur, voici ce que l’on obtient :
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)
Il y a dans cette liste une règle de diagnostic que personne n’énonce, et c’est la chose la plus utile de cette page :
- Un message avec un numéro de position veut dire que vous avez bel et bien du JSON, et qu’il s’arrête tôt ou devient invalide en chemin. Quelque chose a tronqué une vraie charge utile.
- Un message qui cite un extrait et finit par “is not valid JSON” veut dire que ce n’était jamais du JSON. On vous a donné tout autre chose.
- “Unexpected end of JSON input” veut dire que l’analyseur a atteint la fin avant d’avoir une valeur complète, ce qui en pratique signifie que le corps était vide. Les espaces comptent pour du vide.
Ce sont trois bugs différents avec trois corrections différentes, et le texte de l’erreur est la seule chose qui les sépare.
Un corps vide, et les quatre façons d’en obtenir un
“Unexpected end of JSON input” est ce que produit JSON.parse(""), donc la question est pourquoi
le corps était vide.
Une réponse 204 No Content est la version honnête : le serveur vous dit qu’il n’y a rien, et
appeler .json() dessus est le bug. Un 200 au corps vide est la même situation sans
l’avertissement. Un corps déjà consommé le fait aussi : les corps de Response sont des flux
et se lisent une fois, donc await res.text() suivi de await res.json() échoue au second appel
avec exactement ce message. Et une requête interrompue peut vous laisser un corps partiel ou
vide selon l’endroit où elle a été coupée.
Une page web, et c’est le cas fréquent
L’autre message est celui que vous rencontrerez vraiment, et il veut dire que votre requête a renvoyé du HTML.
S’il est si fréquent, c’est à cause d’un détail de fetch sur lequel chacun trébuche une fois :
fetch ne rejette pas sur un 4xx ni un 5xx. Un 404 est un fetch parfaitement réussi du point
de vue de la promesse. Donc si l’URL est fausse, ou si la session a expiré et qu’un proxy a
répondu par une page de connexion, ou si un pare-feu applicatif a servi un défi, ou si le serveur
a levé un 500 et que votre framework a rendu sa propre page d’erreur, res.json() reçoit un
document qui commence par <!DOCTYPE html> et signale le <.
Une page de connexion est la plus pénible, parce qu’elle arrive généralement en 200. Rien n’est faux dans le code de statut. Le corps est simplement un formulaire d’identification.
L’amélioration qui s’arrête à un caractère de l’utilité
Ces messages étaient pires avant. Le ticket 6551 de V8, “JSON.parse errors can be user-friendly
for short strings”, a été ouvert en 2017 et mis en œuvre en mai 2022 : avant,
Unexpected token N in JSON at position 0, et après, "NaN" is not valid JSON. La nouvelle forme
cite ce qu’elle a reçu, ce qui est une vraie amélioration et la raison pour laquelle la formule
“in JSON at position 0” a largement disparu des navigateurs tout en restant dans chaque résultat
de recherche à son sujet.
Cette citation a une limite de longueur, et la mesurer vaut une minute parce que rien ne la documente. Analysez une chaîne de vingt caractères ou moins et le message cite la chaîne entière. À partir de vingt-et-un, il cite les dix premiers caractères et ajoute des points de suspension.
Comptez maintenant les caractères de <!DOCTYPE . Il y en a exactement dix.
Donc pour la cause la plus fréquente de cette erreur, le nouvel extrait si utile est
"<!DOCTYPE "... à chaque fois. Une page 404, une page 500, une redirection vers une connexion et
un défi de pare-feu produisent caractère pour caractère le même message. Récupérer un vrai 404 de
notre propre site, un document HTML de 3 407 octets, produit précisément cela et rien de plus.
Le message a l’air de citer le serveur pour vous. Pour le cas dans lequel vous avez le plus de chances d’être, il cite la seule partie de la réponse que toutes les pages HTML ont en commun.
Et c’est pourquoi la correction n’est jamais dans l’analyseur
Vérifiez la réponse avant de l’analyser, et dites ce que vous avez trouvé quand elle est mauvaise :
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();
Les deux vérifications méritent leur place. La première existe parce que fetch ne lève rien sur
un mauvais statut. La seconde existe parce qu’un 200 peut tout de même être du HTML, ce qui est
exactement le cas de la page de connexion, et parce qu’elle transforme une erreur d’analyseur
illisible en une phrase qui nomme le statut, le type et l’URL.
Et la version qui arrive dans un ticket est pire
Tout ce qui précède suppose que vous pouvez voir la réponse. La personne qui signale le problème, elle, ne peut pas.
Ce qui arrive, c’est “ça dit quelque chose à propos de JSON”, ou au mieux une photo d’une ligne de
console affichant is not valid JSON. Cette ligne ne porte ni le code de statut, ni le
content-type, ni l’URL demandée, ni le corps. Les cinq causes ci-dessus produisent le même texte
de console, et quatre des cinq sont invisibles sans la réponse.
Ce qui fait de cette erreur l’une de celles où la console seule ne suffit pas et où l’onglet réseau est l’artefact qui compte : la requête, son statut, son content-type et les premières centaines d’octets de ce qui est revenu. Ouvrir l’onglet réseau est une minute de travail pour qui l’a déjà fait, et un autre après-midi pour qui ne l’a jamais fait.
Cela vaut la peine de le distinguer de son voisin, parce que les gens cherchent l’un en ayant
l’autre : TypeError: Failed to fetch est la requête qui n’aboutit jamais. Cette erreur
est la requête qui aboutit parfaitement et renvoie la mauvaise chose.
Session Replay
Extension Chrome gratuite. Un clic sur la page qui pose problème enregistre la capture d'écran, la console et le journal réseau, et vous fournit un lien à coller dans le ticket.
Ce qu’il faut en retenir
Le message nomme JSON parce que JSON.parse est l’endroit où votre programme s’est arrêté, pas
celui où il a déraillé. Lisez-le comme une affirmation sur la réponse : vide, ou pas du JSON, ou
tronquée. Puis allez regarder la réponse, car l’analyseur vous a déjà dit tout ce qu’il sait.