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

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

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

中文和 emoji 把 Content-Length 算大了

2026-10-17 / 0 评论 / 7 阅读

中文和 emoji 把 Content-Length 算大了

一个正文里放进中文和 emoji,body.length 就不再等于要发出去的字节数。把前者填进 HTTP 的 Content-Length,接收端可能按较短的长度提前结束正文。Node.js 里这不是边界情况,string.length 和 UTF-8 字节长度本来就在数不同东西。

length 数的是什么?

JavaScript 字符串按 UTF-16 代码单元计数,不按 UTF-8 字节,也不严格按人眼看到的字符计数。'中文'.length 是 2,编码成 UTF-8 后占 6 字节;'😀'.length 也是 2,因为这个码点在 UTF-16 里用一对代码单元表示,UTF-8 则占 4 字节。

混在一起时差距更明显:'中文😀' 的 length 是 4,UTF-8 长度是 10。ASCII 字母在这两种算法下常常给出相同数字,拿它们测试容易让错误看起来没问题。正文一旦包含非 ASCII 内容,就不能再把字符数当作传输长度。

Buffer.byteLength(text, 'utf8') 会计算这段文本编码后占用的字节数。此处的 'utf8' 写清楚编码意图,也便于以后改动代码时看出长度对应哪一种字节序列。若正文已经是 Buffer,则可直接读 .length,那时长度单位已经是字节。

HTTP 头里的数字按什么算?

Content-Length 给出的是消息正文的长度,单位是字节。它不是字符数,也不是 JavaScript 的 UTF-16 代码单元数。头部和正文的传输编码要对应;常见的 JSON 字符串先用 UTF-8 编码,长度就应来自同一串内容的 UTF-8 字节数。

本机用一个回环 HTTP 服务收了一次 POST。请求正文是 {"name":"林😀","note":"中文"}。把字符数当长度会少报 8 字节;脚本实际发送时用 Buffer.byteLength 设置头部,服务端收到的头值是 34,读取到的正文也是 34 字节,内容逐字节相同,响应状态为 200。下面保留脚本原样输出:

node v26.8.1
== JavaScript 字符串长度与 UTF-8 字节数 ==
"中文": length=2 byteLength=6
"😀": length=2 byteLength=4
"中文😀": length=4 byteLength=10
== 本机 HTTP 请求 ==
body.length=26
Buffer.byteLength=34
字符数作为 Content-Length 会少 8 字节
server Content-Length=34
server received bytes=34
server body matches=true
client status=200

这些数字直接来自 tools/verify-utf8-content-length-20261017.mjs。在仓库根目录运行 node tools/verify-utf8-content-length-20261017.mjs 即可重做;服务只监听 127.0.0.1 的临时端口,不会访问外网。脚本同时记录字符长度,并核对服务端收到的正文长度,避免只凭计算式推断请求是否真的完整到达。

为什么错误长度会截断正文?

HTTP 接收方根据 Content-Length 判断正文在哪里结束。若声明值比实际 UTF-8 字节数小,接收方可能在正文尚未完整时就认为消息结束;若声明得更大,接收方则可能继续等待没有到来的字节。连接是否复用、服务器如何报告错误,会影响表面现象,但都不能改变头字段本身的单位。

字符数错误在 ASCII 测试里不容易暴露。'hello' 的 UTF-16 代码单元数和 UTF-8 字节数都是 5,测试结果自然正常。换成中文或 emoji 后数字立刻分开。仅仅检查 JSON 在本地能否 JSON.parse,也证明不了 HTTP 头写对了,因为解析发生在正文已经读出的下一层。

这类偏差还可能出现在日志中的字节统计、文件长度或协议字段。处理文本时要先确定长度字段的单位,再用相应编码求值。Node 的 Buffer.byteLength(text, 'utf8') 适用于尚未编码的字符串;如果编码结果已经放在 Buffer 里,直接使用 buffer.length 更清楚。

我不建议把 string.length 包一层就叫 contentLength,名字会让后续调用者误以为单位是字节。更直白的写法是用 Buffer.byteLength(body, 'utf8') 填头,或者先构造 Buffer,再把同一个 Buffer 交给请求 API 并取它的 .length。关键是计算长度和实际发送的字节来自同一份正文。

字符串长度适合回答 JavaScript 字符串有多少个 UTF-16 代码单元。HTTP Content-Length 则要回答正文编码后有多少字节。两者偶尔相等,不代表它们可以互换。

    🤞 分享