Unexpected token '<' is not valid JSON?接口返回 HTML 而不是 JSON 的 5 种原因
现象:接口「调通了」,数据却解析不了
Uncaught (in promise) SyntaxError: Unexpected token '<', "<html>... is not valid JSON
代码大概长这样:
const res = await fetch('/api/user');
const data = await res.json(); // ← 这里抛错
一行结论:响应体第一个字符是 <,说明服务器返回的是 HTML 页面,res.json() 在第一个字符就解析失败了。这不是 JSON 语法问题,是 HTTP 层出了状况——你要的接口根本没被命中。
和 Unexpected token u(undefined)、Unexpected token ,(尾逗号)不同,'<' 这个 token 几乎总是指向同一个方向:你拿到的是一张网页。
五种原因,按概率排序
1. SPA 路由兜底:404 被重写成 index.html(最常见)
前端项目常见 nginx 配置:
location / {
try_files $uri $uri/ /index.html; # 找不到的路径全部回退到首页
}
/api/user 路径打到了这个 location(比如 /api 的 proxy_pass 忘了配、或路径前缀不一致),nginx 找不到对应文件,就把 index.html 原样返回——状态码还是 200,前端毫无防备地 .json(),炸。
验证:DevTools Network 面板看该请求的 Response,如果是一段完整的 HTML 且含 <div id="root">,就是它。
2. 登录态失效:302 跳到了登录页
服务端鉴权失败返回 302 → 浏览器自动跟随重定向 → 最终拿到登录页 HTML(状态码 200)。fetch 默认 redirect: 'follow',中间的 302 对你完全透明。
// 显式观察重定向行为
const res = await fetch('/api/user', { redirect: 'manual' });
console.log(res.type); // "opaqueredirect" 说明发生了跳转
修法:让后端对 /api/** 返回 401 JSON 而不是 302;前端统一拦截 401 跳登录。
3. 网关错误页:502/503 时 nginx 输出 HTML
后端进程挂了或超时,nginx 返回自带错误页:
<html>
<head><title>502 Bad Gateway</title></head>
...
很多前端的错误处理只 catch 了 json 解析异常,却没看状态码——错误信息完全丢失。
4. baseURL / 端口写错
// 开发环境代理没生效,请求打到了前端 dev server 自己
fetch('/api/user') // ❌ vite proxy 未配置
fetch('http://localhost:3000/api/user') // ❌ 3000 是前端,后端在 8080
环境变量切换(.env.development vs .env.production)拼错前缀是重灾区。验证方法:Network 面板看请求实际 URL,和你以为的对比。
5. 服务端错误分支忘了设 Content-Type
// Express 示例:错误分支返回了 HTML
app.get('/api/user', (req, res) => {
if (!req.session.user) {
return res.redirect('/login'); // ❌ 返回 HTML
}
res.json(user);
});
排查三板斧(按顺序做)
# 1. 绕过前端,直接看原始响应——状态码、Content-Type、前几行内容一目了然
curl -i 'https://your-site.com/api/user' | head -20
# 2. 带上和前端一样的请求头(很多 302 是缺 Cookie/Token 触发的)
curl -i -H 'Cookie: session=xxx' 'https://your-site.com/api/user'
# 3. 只看前 200 字符的响应体
curl -s 'https://your-site.com/api/user' | head -c 200
curl -i 的输出能一次回答三个问题:状态码是多少(200/302/502)、Content-Type 是什么(text/html 还是 application/json)、响应体开头是什么(<!DOCTYPE 还是 {")。九成情况看到输出就知道原因了。也可以直接丢进 API 在线测试工具里看完整响应头。
前端防御性写法:先看 res.ok,再解析
async function safeJson(res) {
const text = await res.text();
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${text.slice(0, 100)}`);
}
try {
return JSON.parse(text);
} catch {
throw new Error(`响应不是 JSON: ${text.slice(0, 100)}`);
}
}
两个关键点:
- 先判断
res.ok/res.status,再调.json()——错误页最常见的伴随状态码是 404/401/502,先看状态码能拿到更准确的报错信息; - 先
.text()再JSON.parse,失败时能打印出响应体前 100 字符,一眼看出是 HTML 还是别的。
和「JSON 语法错误」的区别
如果状态码是 200、Content-Type 也是 application/json,但仍然报 Unexpected token(且 token 不是 <),那就是 JSON 本身的语法问题了——尾逗号、单引号、注释、BOM 头之类,参考另一篇:JSON 解析报错 Unexpected token 的 5 种原因,可以用 JSON 格式化工具精确定位到出错位置。
排查清单
- Network 面板:状态码是多少?200 不代表没问题(SPA 兜底也是 200)
- Response 内容:开头是
<!DOCTYPE html>还是{? curl -i直连:重现吗?带上 Cookie 呢?- 检查 nginx:
/api的proxy_pass在try_files之前匹配吗? - 检查 baseURL:开发/生产环境变量拼对了吗?
- 前端加
res.ok判断 +.text()兜底,别让用户看到裸的 SyntaxError
本文由 ToolVault 工具匣 提供。相关工具:API 在线测试、JSON 格式化校验、HTTP 状态码速查、curl 命令转代码。访问 首页 查看更多开发者工具。