
Qualcosa chiama JSON.parse, direttamente o tramite res.json(), e la console dice una di queste
due cose:
SyntaxError: Unexpected end of JSON input
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Quasi tutti le leggono come una lamentela sul proprio JSON e vanno a guardare il proprio JSON. Il JSON non è quasi mai il problema. Di solito non c’è JSON per niente, e il parser vi sta raccontando la prima cosa che non ha potuto usare.
Questi sono i messaggi che stampa Chrome. Firefox e Safari formulano i loro in modo diverso; le cause qui sotto sono le stesse, da qualunque browser arrivi la segnalazione.
Cosa vi dice davvero ciascun messaggio
Provato in una console del browser, viene fuori questo:
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 quella lista c’è una regola diagnostica che nessuno dice ad alta voce, ed è la cosa più utile di questa pagina:
- Un messaggio con un numero di posizione significa che davvero avete del JSON, e che finisce presto o diventa non valido a metà strada. Qualcosa ha troncato un payload vero.
- Un messaggio che cita un frammento e finisce con “is not valid JSON” significa che non è mai stato JSON. Vi hanno dato qualcosa di completamente diverso.
- “Unexpected end of JSON input” significa che il parser è arrivato alla fine prima di avere un valore completo, che in pratica vuol dire corpo vuoto. Gli spazi contano come vuoto.
Sono tre bug diversi con tre correzioni diverse, e il testo dell’errore è l’unica cosa che li separa.
Un corpo vuoto, e i quattro modi di ottenerne uno
“Unexpected end of JSON input” è quello che produce JSON.parse(""), quindi la domanda è perché
il corpo fosse vuoto.
Una risposta 204 No Content è la versione onesta: il server vi dice che non c’è niente, e
chiamarci .json() sopra è il bug. Un 200 con il corpo vuoto è la stessa situazione senza
l’avvertimento. Un corpo già consumato lo fa anche: i corpi di Response sono stream e si
leggono una volta, quindi await res.text() seguito da await res.json() fallisce alla seconda
chiamata con esattamente questo messaggio. E una richiesta interrotta può lasciarvi un corpo
parziale o vuoto a seconda di dove è stata tagliata.
Una pagina web, che è il caso frequente
L’altro messaggio è quello che incontrerete per davvero, e significa che la vostra richiesta ha restituito HTML.
È così frequente per un dettaglio di fetch su cui tutti inciampano una volta: fetch non
rifiuta su 4xx o 5xx. Un 404 è un fetch perfettamente riuscito per quanto riguarda la promise.
Quindi se l’URL è sbagliato, o la sessione è scaduta e un proxy ha risposto con una pagina di
accesso, o un web application firewall ha servito una verifica, o il server ha lanciato un 500 e
il vostro framework ha reso la propria pagina di errore, res.json() riceve un documento che
inizia con <!DOCTYPE html> e segnala il <.
Una pagina di accesso è la peggiore fra queste, perché di solito arriva come un 200. Nel codice di stato non c’è niente di sbagliato. Il corpo è semplicemente un modulo di login.
Il miglioramento che si ferma a un carattere dall’essere utile
Questi messaggi prima erano peggiori. L’issue 6551 di V8, “JSON.parse errors can be user-friendly
for short strings”, è stato aperto nel 2017 e realizzato nel maggio 2022: prima,
Unexpected token N in JSON at position 0, e dopo, "NaN" is not valid JSON. La forma nuova cita
quello che ha ricevuto, che è un miglioramento vero e il motivo per cui l’espressione
“in JSON at position 0” è in gran parte scomparsa dai browser mentre resta in ogni risultato di
ricerca a riguardo.
Quella citazione ha un limite di lunghezza, e misurarlo vale un minuto perché non è documentato da nessuna parte. Fate il parse di una stringa di venti caratteri o meno e il messaggio cita l’intera stringa. Da ventuno in su cita i primi dieci caratteri e aggiunge dei puntini.
Ora contate i caratteri di <!DOCTYPE . Sono esattamente dieci.
Quindi per la causa più frequente di questo errore il nuovo frammento così utile è
"<!DOCTYPE "... ogni volta. Una pagina 404, una pagina 500, un reindirizzamento al login e una
verifica di firewall producono carattere per carattere lo stesso messaggio. Chiedere un 404 vero
dal nostro sito, un documento HTML di 3.407 byte, produce precisamente quello e nient’altro.
Il messaggio sembra citarvi il server. Per il caso in cui è più probabile che siate, sta citando l’unica parte della risposta che ogni pagina HTML ha in comune.
Ed è per questo che la correzione non è mai nel parser
Controllate la risposta prima di farne il parse, e dite cosa avete trovato quando è sbagliata:
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();
Entrambi i controlli si guadagnano il posto. Il primo esiste perché fetch non lancia su uno stato
cattivo. Il secondo esiste perché un 200 può comunque essere HTML, che è esattamente il caso della
pagina di accesso, e perché trasforma un errore illeggibile del parser in una frase che nomina lo
stato, il tipo e l’URL.
E la versione che arriva in un ticket è peggio
Tutto quello che precede presuppone che possiate vedere la risposta. La persona che segnala il problema non può.
Quello che arriva è “dice qualcosa sul JSON”, o al massimo una foto di una riga di console con
scritto is not valid JSON. Quella riga non porta il codice di stato, né il content-type, né
l’URL richiesto, né il corpo. Tutte e cinque le cause qui sopra producono lo stesso testo in
console, e quattro delle cinque sono invisibili senza la risposta.
Il che fa di questo uno degli errori in cui la console da sola non basta e la scheda di rete è l’artefatto che conta: la richiesta, il suo stato, il suo content-type e le prime centinaia di byte di quello che è tornato. Aprire la scheda di rete è un minuto di lavoro per chi lo ha già fatto e un altro pomeriggio per chi non lo ha fatto.
Vale la pena separarlo dal vicino, perché la gente cerca uno avendo l’altro:
TypeError: Failed to fetch è la richiesta che non arriva mai a compimento. Questo errore
è la richiesta che si compie perfettamente e restituisce la cosa sbagliata.
Session Replay
Estensione Chrome gratuita. Un clic sulla pagina che non si comporta come dovrebbe acquisisce lo screenshot, la console e il network log, e restituisce un link da incollare nel ticket.
Cosa portarsi via
Il messaggio nomina JSON perché JSON.parse è il punto in cui il vostro programma si è fermato,
non quello in cui è andato storto. Leggetelo come un’affermazione sulla risposta: vuota, o non
JSON, o troncata. Poi andate a guardare la risposta, perché il parser vi ha già detto tutto quello
che sa.