Node.js · 原创实验
fetch 已经拿到 200,读取 JSON 为什么还会失败?
同一个本地服务分别返回 404、损坏的 JSON 和半段响应体。两轮运行对照 status、ok、SyntaxError 与 AbortError,区分收到响应和读完正文。
记录时间: (北京时间) · Node v22.22.3 · darwin 27.0.1 / arm64
文章语言: English
日志已经打出 status=200,下一行 await response.json() 却进了 catch。排查时要把这两个 await 分开看:第一个拿到响应对象,第二个还要读取正文并解析 JSON。
本地服务准备了三个地址:一个返回 404 和完整 JSON,一个返回 200 但正文不是完整 JSON,还有一个返回 200 后只写半段正文。第三组等 fetch 返回,再主动发出 abort。两轮里,它们分别表现为正常返回 404 响应、SyntaxError 和 AbortError。
两轮实际结果
| 情况 | 第 1 轮 | 第 2 轮 |
|---|---|---|
| 404 加完整 JSON | fetch 返回响应;status=404;ok=false | fetch 返回响应;status=404;ok=false |
| 200 加损坏 JSON | status=200;json() 拒绝:SyntaxError | status=200;json() 拒绝:SyntaxError |
| 增加显式状态检查的封装 | 显式检查后:HTTP 404;损坏 JSON:SyntaxError | 显式检查后:HTTP 404;损坏 JSON:SyntaxError |
| 收到 200 后取消正文读取 | 已收到 status=200;abort 后 json() 拒绝:AbortError | 已收到 status=200;abort 后 json() 拒绝:AbortError |
404 会交给状态检查处理
第一组的 fetch 正常返回了 Response。读取到的 status 是 404,ok 是 false,正文也成功解析成带 error 的对象。只包一层 try/catch,不能由此认定每次 HTTP 错误都会变成网络异常。
应用需要决定哪些状态可以继续处理。实验里的 getJson 在 !response.ok 时取消剩余正文读取,再抛出带状态码的错误。用它请求同一个地址,结果变成 HTTP 404。
200 不替 JSON 语法作保证
第二个地址设置了 application/json,并返回 200,但正文只有一个未完成的对象开头。响应传输结束后,json() 仍然因语法不完整而拒绝。
脚本只断言错误名 SyntaxError,没有把错误消息的具体措辞写死。排查这种问题时,可以记录状态、内容类型和受控长度的正文片段;带有令牌或用户数据的响应不要直接整段打进日志。
response.writeHead(200, { "Content-Type": "application/json" });
response.end('{"incomplete":');
// fetch 可以返回 200;json() 在本次运行中抛出 SyntaxError。拿到响应后,正文仍然可以被取消
第三个地址发出响应头和半段 JSON 后保持连接。客户端先得到 200,再启动 json(),随后调用 controller.abort()。本次正文读取以 AbortError 拒绝。
这能解释一种排查顺序:先确认请求是否收到响应,再确认正文是否读完,最后检查解析结果。下载到一半被取消与完整正文语法错误,后续处理不应只剩下同一个“请求失败”标签。
const response = await fetch(url, { signal: controller.signal });
const pendingBody = response.json();
controller.abort();
await pendingBody; // 本次以 AbortError 拒绝让同一个超时信号覆盖正文读取
脚本里的 getJson 把 5000 毫秒的超时信号传给 fetch,并在同一个异步操作里等待 JSON。它还会先检查 HTTP 状态。两轮验证中,404 被转成显式 HTTP 错误,损坏 JSON 继续保留 SyntaxError。
半段正文那组使用主动 abort 控制顺序,没有等待满 5 秒,也没有测真实断网。这个实验确认的是正文读取仍有失败阶段,不能把收到 200 当成整个操作已经完成。
async function getJson(url) {
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (!response.ok) {
await response.body?.cancel();
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}下载脚本,在自己的机器上复跑
使用 Node.js 22 或更高版本,不需要安装依赖。脚本只使用 Node 内置模块;每次执行都会跑两轮并检查断言。记录路径必须尚不存在,避免覆盖上次结果。
node --version
node run.mjs
node run.mjs --record my-results.json请求只发给本机随机端口的测试服务。没有测浏览器跨域、代理、TLS、实际断网或远程服务的业务取消;客户端停止读取不能作为服务端业务已回滚的依据。
这是同一操作者在本机完成的两轮记录,尚无独立读者复跑反馈。换环境后请保留自己的输出;断言失败会终止脚本。
核对脚本 SHA-256
45ba61e8fdad5dec4e37ce3ecabb7690b8ed7515f663396c28fb878b176ffba1
摘要对应页面提供的脚本文件内容。完整运行数据在 verification.json。