← Tutti gli articoli

Unexpected Token < in JSON alla posizione 0: hai ricevuto HTML

L'errore «Unexpected token <» significa che JSON.parse ha ricevuto una pagina HTML (un 404, un redirect di login o URL sbagliato), non JSON. Il perché, con esempi fetch rotti e corretti.

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— significa quasi sempre una sola cosa: il tuo codice ha chiamato JSON.parse() (o res.json()) su una pagina HTML, non JSON. Il < in posizione 0 è l'apertura di <!DOCTYPE html> o <html>. Ecco esattamente perché succede e come risolverlo.

Come appare l'errore

// V8 (Chrome / Node / Edge)
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
// V8 più vecchio
SyntaxError: Unexpected token < in JSON at position 0

// Firefox
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

// Safari
SyntaxError: JSON Parse error: Unexpected identifier "<"

Posizione 0 è l'indizio chiave: il primissimo byte è già sbagliato, quindi la risposta non è mai stata JSON fin dall'inizio.

Perché succede: hai ricevuto HTML

Una chiamata fetch o AJAX ha restituito un documento HTML invece del JSON atteso. Fonti tipiche:

  • Una pagina di errore 404 / 500 servita come HTML dal tuo framework o server web
  • Un redirect di login o auth verso una pagina HTML (sessione scaduta)
  • Una URL sbagliata —— stai colpendo la route dell'app/SPA invece dell'endpoint API
  • Un proxy, CDN o captive portal che restituisce la sua pagina HTML

Esempio rotto

const res  = await fetch('/api/user');   // il server ha restituito una pagina HTML 404
const data = await res.json();           // ❌ Unexpected token '<' …
// res.json() prova a parsare "<!DOCTYPE html>…" come JSON

Esempio corretto

const res = await fetch('/api/user');

// 1) Controlla lo status prima di parsare
if (!res.ok) {
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
}

// 2) Opzionale: conferma che sia davvero JSON
const type = res.headers.get('content-type') ?? '';
if (!type.includes('application/json')) {
  const text = await res.text();
  throw new Error(`Expected JSON, got: ${text.slice(0, 80)}…`);
}

const data = await res.json();           // ✅ sicuro

Due fonti meno ovvie che vale la pena conoscere

  • Risposte bloccate da WAF o rate-limit. Un WAF, CDN o livello di protezione DDoS può intercettare la chiamata e restituire la sua pagina HTML di challenge / blocco —— l'interstitial Cloudflare, la pagina di rifiuto di AWS WAF, il muro captive-portal del Wi-Fi pubblico. Lo status è spesso 403 / 429 / 503 con body HTML. Ispeziona la risposta nel tab Network e guarda se ci sono header come CF-Ray o X-Amz-Cf-Id per individuarlo.
  • Il fallback di history della SPA che restituisce index.html. Host statici e dev-server spesso riscrivono qualsiasi path sconosciuto verso /index.html perché il router client prenda il controllo. Se il tuo front-end per sbaglio fa fetch di /api/user come path relativo e il proxy non è configurato, il server restituisce silenziosamente la shell HTML della tua SPA con un 200 —— e ti ritrovi una versione confondente «200 OK ma è HTML» dello stesso errore.

Come risolvere —— passo dopo passo

  1. Logga il body grezzo:console.log(await res.clone().text()). Se inizia con <!DOCTYPE o <html>, è HTML.
  2. Controlla res.ok / res.status prima di chiamare res.json() —— un 4xx/5xx tipicamente restituisce una pagina HTML di errore.
  3. Verifica l'URL nel tab Network. Un path relativo potrebbe risolvere verso l'index.html della tua SPA invece dell'API.
  4. Controlla l'autenticazione —— un redirect alla pagina di login significa che il tuo token/sessione è scaduto.
  5. Ispeziona Content-Type —— diramati su di esso per gestire le risposte non-JSON invece di parsarle alla cieca.

Domande frequenti

Cosa significa «Unexpected token < in JSON at position 0»?

Il parser ha trovato < come primo carattere, cosa che non è mai JSON valido. La risposta era HTML (una pagina <!DOCTYPE> o <html>), non il JSON che il tuo codice si aspettava.

Perché il mio fetch restituisce HTML invece di JSON?

Cause comuni: pagina di errore 404/500, redirect di auth verso una pagina di login, o URL sbagliata che risolve al tuo front-end invece che all'API. Controlla res.ok e logga await res.text() per vedere cosa è tornato.

Come evito che rompa la mia app?

Fai guard prima di parsare: controlla res.ok e l'header Content-Type, e avvolgi JSON.parse()/res.json() in try/catch così una risposta HTML diventa un errore gestito invece che un'eccezione.

Risolvi subito

Se hai un body di risposta su cui sei in dubbio, incollalo in JSON Fix —— ti dice all'istante se è JSON valido o qualcos'altro (come HTML). Tutto gira nel tuo browser.