← Todos os artigos

Unexpected Token < em JSON na posição 0: você recebeu HTML

O erro «Unexpected token <» significa que JSON.parse recebeu uma página HTML (um 404, um redirecionamento de login ou URL errada), não JSON. O porquê, com exemplos de fetch quebrados/corrigidos.

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— quase sempre significa uma coisa: seu código chamou JSON.parse() (ou res.json()) em uma página HTML, não JSON. O < na posição 0 é a abertura de <!DOCTYPE html> ou <html>. Aqui está exatamente por que isso acontece e como consertar.

Como o erro aparece

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

Posição 0 é a pista chave: o primeiro byte já está errado, então a resposta nunca foi JSON desde o começo.

Por que acontece: você recebeu HTML

Uma chamada fetch ou AJAX devolveu um documento HTML em vez do JSON esperado. Origens comuns:

  • Uma página de erro 404 / 500 servida como HTML pelo seu framework ou servidor web
  • Um redirecionamento de login ou auth para uma página HTML (sessão expirada)
  • Uma URL errada —— batendo na rota do app/SPA em vez do endpoint da API
  • Um proxy, CDN ou captive portal devolvendo sua própria página HTML

Exemplo quebrado

const res  = await fetch('/api/user');   // servidor devolveu uma página HTML 404
const data = await res.json();           // ❌ Unexpected token '<' …
// res.json() tenta parsear "<!DOCTYPE html>…" como JSON

Exemplo consertado

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

// 1) Cheque o status antes de parsear
if (!res.ok) {
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
}

// 2) Opcional: confirme que é realmente 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();           // ✅ seguro

Duas origens menos óbvias que vale conhecer

  • Respostas bloqueadas por WAF ou rate-limit. Um WAF, CDN ou camada de proteção DDoS pode interceptar a chamada e devolver sua própria página HTML de desafio / bloqueio —— o interstitial do Cloudflare, a página de negação do AWS WAF, a parede de captive-portal de Wi-Fi público. O status geralmente é 403 / 429 / 503 com corpo HTML. Inspecione a resposta na aba Network e veja se há headers como CF-Ray ou X-Amz-Cf-Id para detectar.
  • O fallback de history da SPA devolvendo index.html. Hosts estáticos e dev-servers frequentemente reescrevem qualquer rota desconhecida para /index.html para que o router do cliente assuma. Se seu front-end por acidente fizer fetch de /api/user como caminho relativo e o proxy não estiver configurado, o servidor silenciosamente devolve a casca HTML da sua SPA com 200 —— e você acaba com uma versão confusa de „200 OK mas é HTML" do mesmo erro.

Como consertar —— passo a passo

  1. Logue o body cru:console.log(await res.clone().text()). Se começa com <!DOCTYPE ou <html>, é HTML.
  2. Cheque res.ok / res.status antes de chamar res.json() —— um 4xx/5xx tipicamente devolve uma página HTML de erro.
  3. Verifique a URL na aba Network. Um caminho relativo pode resolver para o index.html da sua SPA em vez da API.
  4. Cheque a autenticação —— um redirecionamento para a página de login significa que seu token/sessão expirou.
  5. Inspecione Content-Type —— ramifique sobre ele para lidar com respostas não-JSON em vez de parseá-las às cegas.

Perguntas frequentes

O que significa „Unexpected token < in JSON at position 0"?

O parser encontrou < como primeiro caractere, o que nunca é JSON válido. A resposta era HTML (uma página <!DOCTYPE> ou <html>), não o JSON que seu código esperava.

Por que meu fetch retorna HTML em vez de JSON?

Causas comuns: página de erro 404/500, redirecionamento de auth para uma página de login, ou URL errada que resolve para seu front-end em vez da API. Cheque res.ok e logue await res.text() para ver o que voltou.

Como evito que isso quebre meu app?

Guard antes de parsear: cheque res.ok e o header Content-Type, e envolva JSON.parse()/res.json() em try/catch para que uma resposta HTML vire um erro tratado em vez de uma exceção.

Conserte agora

Se você tem um body de resposta sobre o qual está em dúvida, cole no JSON Fix —— ele te diz instantaneamente se é JSON válido ou outra coisa (como HTML). Tudo roda no seu navegador.