
接口文档把成功但无数据写成 204,调用端却在 await response.json() 这一行报错。看上去像请求失败,实际请求已经完成:fetch() 给回了 Response,错发生在读取响应体并把它解析成 JSON 的时候。两件事挤在一行代码里,很容易把责任归错。
1. 在本机准备三个响应
验证不需要连外网。脚本用 Node 的 http 模块监听 127.0.0.1 随机端口,分别提供 204 无内容、200 空体和 200 合法 JSON。随机端口由操作系统分配,退出时服务端会关闭;运行过程没有固定端口冲突,也没有依赖线上接口。
node tools/verify-fetch-empty-body.mjs
脚本对前两个地址先打印 fetch 返回的状态和 ok,随后调用 json() 并捕获异常;另外对 204 读取一次 text(),最后请求合法 JSON 作对照。每个响应体只能消费一次,因此文本检查另发了一次请求,不会拿已经读过的 Response 再读。
2. 看清楚异常落在哪一层
本机 Node v26.8.1 的原样输出如下:
204: fetch status=204, ok=true
204: json throws SyntaxError: Unexpected end of JSON input
200 empty: fetch status=200, ok=true
200 empty: json throws SyntaxError: Unexpected end of JSON input
204: text=""
200 JSON: fetch status=200, json={"ok":true}
204 的 fetch 已经成功返回,状态是 204,ok 也是 true。没有抛错的是请求阶段;抛出 SyntaxError 的是下一步 json()。错误原文是 Unexpected end of JSON input,指向的正是空输入无法组成 JSON 值。
200 空体也抛出完全相同的异常。这个对照很有用:解析器看到的是空内容,不会因为 HTTP 状态是 204 就自动返回 null。反过来,204 的 text() 结果是空字符串;合法 JSON 那次则正常解析为对象。状态、文本读取和 JSON 解析各有各的结果,别把它们压成一个"请求成功或失败"的判断。
response.ok 只说明响应状态落在成功范围,并不表示响应体一定有内容,更不表示它一定是 JSON。这个检查与媒体类型也不是一回事。脚本里的空 200 特意带了 application/json,json() 仍因零字节输入失败;请求头声明格式,不能替服务端补出内容。真实调用如果要解析 JSON,状态和响应体都要符合接口约定。
3. 按接口契约决定解析方式
如果接口明确用 204 表示成功但没有数据,先识别这个状态,再跳过 JSON 解析:
const response = await fetch(url);
if (response.status === 204) {
return null;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
return null 是调用层的约定,不是 204 自动产生的 JSON 值。若接口约定的是 JSON,服务端就应返回实际 JSON 文本,并使用相应的成功状态;空字符串不代表 JSON 的 null,后者必须是四个字符 null。如果响应体可能为空,但状态并不固定,也应由接口约定明确空值如何表示,再在解析前分支处理。
我会保留这个分支,而不是用 try/catch 把所有解析错误都变成 null。后者会同时吞掉截断 JSON、错误格式等真正需要排查的问题。只有当接口契约明确规定该状态无响应体时,按状态跳过解析才够窄;其余响应仍交给 JSON 解析器正常报错。
4. 把空值定义留在接口边界
调用端返回 null 之后,下游代码会把它当作一种正常结果。因此这个约定最好和接口的状态码一起写清楚:204 表示无内容,200 则应说明返回何种 JSON 结构。不要仅凭 ok 推断后续一定有可解析的对象,也不要把 json() 的任何异常都转成空值。网络中断或 JSON 文本被截断,处理方式都可能不同;一概吞掉之后,调用方只剩一个看似正常的 null,定位信息也丢了。
这个小脚本刻意保留了空 200 作为对照,避免把问题误认成 204 的专属异常。修改调用代码后,可以用同一组本机响应再跑一遍,确认 204 按契约返回空值,同时合法 JSON 仍可解析,空的 JSON 响应仍显式失败。验证覆盖的只是当前 Node 运行时和这三个本地响应,不代表所有浏览器或其他运行时会给出相同的异常文案。