← Tous les articles

Unexpected Token < en JSON à la position 0 : vous avez du HTML

L'erreur « Unexpected token < » signifie que JSON.parse a reçu une page HTML (un 404, une redirection de login, une mauvaise URL), pas du JSON. Voici le pourquoi, avec des exemples fetch cassés/corrigés.

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— cela signifie presque toujours une seule chose : ton code a appelé JSON.parse() (ou res.json()) sur une page HTML, pas du JSON. Le < en position 0 est l'ouverture de <!DOCTYPE html> ou <html>. Voici exactement pourquoi ça arrive et comment le corriger.

À quoi ressemble l'erreur

// V8 (Chrome / Node / Edge)
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
// V8 plus ancien
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 "<"

Position 0 est l'indice clé : le tout premier octet est déjà mauvais, donc la réponse n'était jamais du JSON à la base.

Pourquoi ça arrive : tu as reçu du HTML

Un appel fetch ou AJAX a renvoyé un document HTML au lieu du JSON attendu. Sources habituelles :

  • Une page d'erreur 404 / 500 servie en HTML par ton framework ou serveur web
  • Une redirection login ou auth vers une page HTML (session expirée)
  • Une mauvaise URL —— tu tapes sur la route app/SPA au lieu du endpoint API
  • Un proxy, CDN ou captive portal qui renvoie sa propre page HTML

Exemple cassé

const res  = await fetch('/api/user');   // le serveur a renvoyé une page HTML 404
const data = await res.json();           // ❌ Unexpected token '<' …
// res.json() essaie de parser "<!DOCTYPE html>…" comme du JSON

Exemple corrigé

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

// 1) Vérifie le statut avant de parser
if (!res.ok) {
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
}

// 2) Optionnel : confirme que c'est vraiment du 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();           // ✅ sûr

Deux sources moins évidentes qu'il vaut la peine de connaître

  • Réponses bloquées par WAF ou rate-limit. Un WAF, CDN ou couche de protection DDoS peut intercepter l'appel et renvoyer sa propre page HTML de challenge / blocage —— l'interstitiel Cloudflare, la page de refus AWS WAF, le mur captive-portal d'un Wi-Fi public. Le statut est souvent 403 / 429 / 503 avec un corps HTML. Inspecte la réponse dans l'onglet Network et regarde si des headers comme CF-Ray ou X-Amz-Cf-Id sont présents pour le détecter.
  • Le fallback d'historique SPA qui renvoie index.html. Les hébergeurs statiques et serveurs de dev réécrivent souvent tout chemin inconnu vers /index.html pour que le router client prenne le relais. Si ton front-end fetch /api/user en chemin relatif par accident et que le proxy n'est pas configuré, le serveur renvoie silencieusement la coquille HTML de ta SPA avec un 200 —— tu obtiens alors une version déroutante « 200 OK mais c'est du HTML » de la même erreur.

Comment le corriger —— étape par étape

  1. Log le corps brut :console.log(await res.clone().text()). S'il commence par <!DOCTYPE ou <html>, tu as du HTML.
  2. Vérifie res.ok / res.status avant d'appeler res.json() —— un 4xx/5xx renvoie typiquement une page HTML d'erreur.
  3. Vérifie l'URL dans l'onglet Network. Un chemin relatif peut se résoudre vers l'index.html de ta SPA au lieu de l'API.
  4. Vérifie l'authentification —— une redirection vers la page de login signifie que ton token/session a expiré.
  5. Inspecte Content-Type —— branche dessus pour gérer les réponses non-JSON au lieu de les parser aveuglément.

Foire aux questions

Que signifie « Unexpected token < in JSON at position 0 » ?

Le parser a rencontré < comme premier caractère, ce qui n'est jamais du JSON valide. La réponse était du HTML (une page <!DOCTYPE> ou <html>), pas le JSON que ton code attendait.

Pourquoi mon fetch renvoie-t-il du HTML au lieu de JSON ?

Causes courantes : page d'erreur 404/500, redirection d'auth vers une page de login, ou mauvaise URL qui se résout vers ton front-end au lieu de l'API. Vérifie res.ok et log await res.text() pour voir ce qui est revenu.

Comment empêcher ça de casser mon app ?

Guard avant de parser : vérifie res.ok et le header Content-Type, et enveloppe JSON.parse()/res.json() dans un try/catch pour qu'une réponse HTML devienne une erreur gérée plutôt qu'une exception.

Corrige-le maintenant

Si tu as un corps de réponse dont tu n'es pas sûr, colle-le dans JSON Fix —— il te dit instantanément si c'est du JSON valide ou autre chose (comme du HTML). Tout tourne dans ton navigateur.