DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

fetch 先 text 再 json 报 Body is unusable:正文已经被读取

text和json消费同一个Response正文。检查日志与reader锁,只读一次再解析,区分已消费、被锁及空正文解析错误,保护日志内容。

接口已经返回 Response,为调试先输出 await response.text(),随后执行 await response.json() 却报 Body is unusable。这两次调用消费的是同一条正文流,不会各自重新向服务器获取一份内容。

Node.js 24.10.0 内置 Undici 的错误文字包含 Body has already been read。不同版本的文字可能较短,但排查重点相同:是否已开始消费正文,或有没有 reader 正持有它的锁。

调试读取也会消耗正文

下面是导致重复读取的代码片段:

const response = await fetch(url);
const debugText = await response.text();
const data = await response.json();

text() 完成以后,json() 不能再消费同一个正文。先检查中间件、日志组件和错误处理:第一次读取可能发生在它们内部,不一定就在报错上一行。

response.json() 也不是读取已缓存字符串的解析工具,它先读流,再解析 JSON。第一次 json 解析失败以后改用 response.text(),通常也不能取回原正文,因为读流阶段已经发生。

只读取一次,再使用结果

只需要 JSON 数据时,保留一次 response.json(),日志从已经解析的对象中挑选必要且可公开的字段,不为记录日志再读取整个 Response。

如果确实需要检查原始文本,并且这个响应预期为 JSON,可以先读一次文本,再自己解析:

const rawText = await response.text();
const data = JSON.parse(rawText);

这是已有 Response 的处理片段,仍应接入应用原来的 HTTP 状态检查和异常处理。JSON.parse 遇到空内容或非法 JSON 会抛解析错误,不要把它统一包装成“正文已读取”。诊断原始文本时做脱敏和长度限制;响应可能包含令牌、个人信息或体积很大的内容。

不要为得到调试副本再自动发一次请求。请求可能修改业务状态,两次返回也未必相同;修正正文所有权和消费位置,才能解决重复读取。

bodyUsed 为 False 也不能保证可读

response.bodyUsed 可用于观察正文是否已被扰动,但流被 reader 锁定时,它仍可能为 False。response.body?.locked 能帮助识别另一条条件;这些状态只能辅助定位,不能让多个消费者竞相读取同一流。

如果代码获取了 response.body.getReader(),应由这一条消费路径负责读取和释放锁。reader 已经读过一部分后,即使释放锁,也不会让普通 text/json 方法重新得到完整原始正文。先检查是谁取得 reader,再决定由哪一层统一处理内容。

没有正文的响应也不是同一根因:对空字节执行 json 解析可能报解析错误。接口返回 204、HTML 错误页或其他非 JSON 格式时,分别核对 HTTP 状态和响应契约,不继续重复调用读取方法试格式。

验收应覆盖原来的业务调用和日志入口,确认正文只消费一次、格式错误仍按应用约定处理,日志没有泄露内容。若错误发生在收到响应之前,可继续检查fetch 的连接阶段错误,不要拿正文处理修改去解决连接超时。