侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 270 篇文章
  • 累计收到 7 条评论

Response.json 遇 BOM 还会失败吗

2026-11-14 / 0 评论 / 16 阅读

Response.json 遇 BOM 还会失败吗

直接解析为什么报错

一段 JSON 文本前面多出三个字节 EF BB BF,肉眼打开文件时往往看不出来。服务端可能把它们放在响应体开头,客户端拿到的却是带不可见字符的字符串。把这份字符串交给 JSON.parse,Node 26.8.1 抛出 SyntaxError: Unexpected token ''。错误位置看起来像 JSON 的第一个字符,实际挡在前面的,是 UTF-8 BOM 解码成的 U+FEFF。

tools/verify-json-fetch-bom-20261104.mjs 在本机启动一个只绑定 127.0.0.1 的 HTTP 服务,分别返回普通 JSON 和带 BOM 的响应。脚本先用 arrayBuffer() 取回原始字节,再用 Buffer.toString('utf8') 转成字符串交给 JSON.parse。它另外对同一个带 BOM 的端点调用 text() 和 json(),并用断言检查结果。实测不是远端服务,也没有发起互联网请求。

fetch 的两个读取方法做了什么

日志里的前三个字节确实是 ef bb bf,所以 BOM 并没有在传输中消失。区别发生在解码阶段:Response.text() 返回字符串时,首字符是 {,对应码点 U+007B;Response.json() 对同一响应得到 { "ok": true, "name": "雪" } 这个对象。去掉 BOM 的普通响应也得到相同对象。

容易混淆的是响应体里带 BOM,与交给 JSON 解析器的字符串仍以 BOM 开头,这两种状态并不相同。fetch 的 text()、json() 读取路径会先按 UTF-8 解码,解码过程处理开头的 BOM;Buffer.toString('utf8') 则保留这个字符。后者再直接 JSON.parse,就复现了失败。脚本同时保留原始字节断言和两种解析结果,避免把字符显示问题误判成服务器没有发 BOM。

runtime: node v26.8.1
server: 127.0.0.1:57477 (loopback only)
bom response prefix bytes: ef bb bf
JSON.parse(Buffer.toString('utf8')): SyntaxError: Unexpected token ''
Response.text() first code point: U+007B
Response.json() with BOM: {"ok":true,"name":"雪"}
Response.json() without BOM: {"ok":true,"name":"雪"}
assertion: PASS (BOM bytes, direct parse failure, fetch text/json behavior)

哪种写法会踩中它

常见的绕路是为了记录响应内容,先取成文本,再手动解析。下面的写法在本次实测中可以工作,因为字符串来自 Response.text():

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

在本次 Node 实测里,text() 的首码点已经是 {,这段流程不会因为 BOM 出错。但它仍有一个实际边界:若内容来自文件、缓存或 Buffer 中的原始 UTF-8 字节,调用 Buffer.toString() 后开头的 U+FEFF 会留下来。把两种来源混为一谈,容易写出 fetch 收到 BOM 必然解析失败这样的错误结论。排查时先确认拿到的是响应对象、解码后的文本,还是原始字节;这一步能直接区分解析层和解码层。

接口层直接消费 JSON 时,await response.json() 更清楚,也少一次字符串和对象之间的手工转换。确实需要拿文本做签名、审计或错误记录时,再明确字节从哪来、是否需要保留 BOM。若数据来自文件或 Buffer,清理时应只处理字符串开头的 \uFEFF,不要对整段内容做全局替换,否则正文里的同码点也会被改掉。

BOM 的责任边界

BOM 是编码标记,不属于 JSON 数据字段。服务端最好按 UTF-8 输出不带 BOM 的 JSON,客户端也不该靠猜测响应体的开头字节来判断内容类型。本机脚本证明的是 Node v26.8.1 这条读取路径,不能据此推断所有旧版运行时或第三方 HTTP 库的行为;旧环境要在自己的运行版本上复测。这个差异也影响日志设计:如果日志只保存解码后的文本,原始字节里是否有 BOM 就无法再从日志还原。需要排查编码问题时,可以在读取边界记录字节前缀或码点,但不必把完整响应体写进日志。

我会把 Response.json() 作为普通接口响应的默认入口,不会为它额外加一层删 BOM 的代码。只有接入方明确给的是原始文本或字节,且来源确实可能带 BOM,才在边界做一次有针对性的处理。这样既保留了 fetch 已经完成的解码行为,也不把一次字符编码问题扩大成全局字符串清洗。

    🤞 分享