← 全部文章

JSON 第 0 位 Unexpected Token <:你拿到的是 HTML

“Unexpected token <” 错误表示 JSON.parse 收到的是一个 HTML 页面(404、登录跳转或错误的 URL),而不是 JSON。这里讲清楚原因,附上坏的和修好的 fetch 示例。

SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON —— 几乎总是意味着一件事:你的代码对一个 HTML 页面(不是 JSON)调用了 JSON.parse()(或 res.json())。位置 0 处的 <<!DOCTYPE html><html> 的开头。下面讲清楚为什么会发生以及怎么修。

错误长什么样

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

位置 0 是关键线索:第一个字节就已经错了,说明那段响应根本就不是 JSON。

为什么会这样:你拿到的是 HTML

一个 fetch 或 AJAX 调用返回了一份 HTML 文档,而不是你期望的 JSON。常见来源:

  • 你的框架或 Web 服务器以 HTML 形式提供的一个 404 / 500 错误页
  • 一个跳到 HTML 登录页的 登录或鉴权重定向(会话过期)
  • 用了错误的 URL —— 打到了 app/SPA 路由而不是 API endpoint
  • 一个代理、CDN 或 captive portal 返回了自己的 HTML 页面

坏掉的例子

const res  = await fetch('/api/user');   // 服务器返回了 404 HTML 页
const data = await res.json();           // ❌ Unexpected token '<' …
// res.json() 试图把 "<!DOCTYPE html>…" 当成 JSON 来解析

修好的例子

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

// 1) 解析前先检查状态
if (!res.ok) {
  throw new Error(`HTTP ${res.status} ${res.statusText}`);
}

// 2) 可选:确认它真的是 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();           // ✅ 安全

另外两个不那么明显但值得知道的来源

  • WAF 拦截或限流的响应。 一个 WAF、CDN 或 DDoS 防护层可能拦下调用,并返回它自己的 HTML 挑战 / 拦截页 —— Cloudflare 的中间页、AWS WAF 的拒绝页、公共 Wi-Fi 的 captive-portal 拦截。状态码常常是 403 / 429 / 503 加 HTML body。在 Network 面板里检查响应,看是不是有 CF-Ray X-Amz-Cf-Id 这类 header 能识别出来。
  • SPA 历史回退返回 index.html 静态主机和开发服务器经常把每一个未知路径重写到 /index.html,让客户端路由接手。如果你的前端不小心以相对路径 fetch 了 /api/user 而代理没配好,服务器就会安静地返回你 SPA 的 HTML 壳,状态是 200 —— 你就拿到一个让人疑惑的「200 OK 但其实是 HTML」版本的同一个错误。

怎么修 —— 一步步来

  1. 把原始 body 打印出来:console.log(await res.clone().text())。如果以 <!DOCTYPE<html> 开头,就是 HTML。
  2. 在调用 res.json() 之前先检查 res.ok / res.status —— 4xx/5xx 通常会返回一个 HTML 错误页。
  3. 在 Network 面板里确认 URL。相对路径可能解析到了你 SPA 的 index.html,而不是 API。
  4. 检查认证 —— 跳到登录页意味着你的 token/会话过期了。
  5. Content-Type —— 按它分支处理,避免对非 JSON 响应也盲目解析。

常见问题

「Unexpected token < in JSON at position 0」是什么意思?

解析器把 < 当成了第一个字符,这在任何合法 JSON 里都不可能。响应其实是 HTML(一个 <!DOCTYPE><html> 页面),不是你代码期望的 JSON。

为什么我的 fetch 返回的是 HTML 而不是 JSON?

常见原因:404/500 错误页、跳到登录页的认证重定向,或者 URL 写错了,请求被解析到前端而不是 API。检查 res.ok,打 await res.text() 看看到底返回了什么。

怎么让它不把我的应用搞崩?

解析前先 guard:检查 res.okContent-Type header,并把 JSON.parse()/res.json() 包在 try/catch 里,让一个 HTML 响应变成一个可处理的错误,而不是异常。

立刻修复

如果你有一段响应 body 拿不准,贴到 JSON Fix —— 它会立刻告诉你这到底是合法 JSON,还是别的什么东西(比如 HTML)。全部在浏览器里跑。