Express 5.3.0 发送 Uint16Array 字节变少,关闭 ETag 后报错
Express 5.3.0 直接发送 Uint16Array,可能丢失高位字节或报 ERR_INVALID_ARG_TYPE。按子视图的字节范围复制为 Buffer,避免发送范围外的数据。
Express 接口返回 Uint16Array,HTTP 200 却只有预期一半的字节;关闭 ETag 后,小数组反而报 ERR_INVALID_ARG_TYPE。如果应用在 res.send() 中直接传入非 Uint8Array 的 TypedArray,先明确接口需要发送的是数值列表,还是视图覆盖的原始字节。
Express 5.3.0 的这条路径有两种表现:默认需要生成 ETag,或数组长度达到 1000 时,数据会先经过 Buffer.from(chunk);不生成 ETag 且较小时,则可能直接传给 Node 的响应结束方法。两条路都不能保证把 Uint16Array 按原始内存正确发出。
Buffer.from 数组,与 Buffer.from 底层内存不是一回事
当 view 是 Uint16Array 实例时,Buffer.from(view) 会逐元素生成字节,每个元素只保留一个字节。例如一个 Uint16 元素占两个字节,不能把元素数量直接当作它的原始字节长度。
另一条直接发送的路径也不安全。Node 的响应写入接受字符串、Buffer 或 Uint8Array,不接受任意多字节 TypedArray;于是同一份数据会随 ETag 和数组大小,在静默截断与 500 之间变化。调整 ETag 只能改变分支,不是数据修复。
要传原始字节,明确限定视图范围
如果协议本来约定接收这段原始二进制,可以先建立一字节视图,再复制为 Buffer。下面是已有 Express app 上的独立路由示例:
app.get('/binary', (req, res) => {
const values = new Uint16Array([0x0102, 0x0304]);
const view = values.subarray(1);
const bytes = new Uint8Array(
view.buffer,
view.byteOffset,
view.byteLength,
);
res.type('application/octet-stream').send(Buffer.from(bytes));
});
示例只发送第二个 Uint16 元素所在的两个字节。byteOffset 和 byteLength 都要保留;只写 Buffer.from(view.buffer) 会把这个子视图之外的底层数组也发出去,可能泄露邻接数据。
最后的 Buffer.from(bytes) 复制了所选字节,避免响应尚未消费完时,其他代码修改原可写视图。若为了减少复制改成共享底层内存的 Buffer,应用就必须负责缓冲区直到发送结束的生命周期和不变性,不能边发送边复用它。
原始字节仍采用这块内存的排列方式。Uint16Array 的本机字节序不等于网络协议规定的字节序;协议若要求固定大端或小端,应按协议逐项编码,不能把建立字节视图当作通用序列化。
如果客户端想得到的是数值列表,应采用 JSON 数值列表等原有协议,例如对已经校验的数值视图使用 res.json(Array.from(view))。这与发送二进制不同,不应为了消除异常偷偷更换 Content-Type 和响应格式。
长度与内容都要从接收端核对
在隔离入口覆盖小数组、达到 1000 元素的数组、非零偏移子视图,以及原来启用和不启用 ETag 的配置。按视图 byteLength 核对收到的完整字节,并比较内容,不能只检查 Content-Length 或状态码。
这项处理针对 Express 5.3.0 接受任意 ArrayBuffer view 后的转换分支,不说明所有 Express 版本都有同一二进制行为。DataView、普通 ArrayBuffer 和数值列表各有自己的输入契约,不能只改一处后推断它们都已支持。修正依赖或发送边界,也不会补回此前已经传给客户端的截断结果。


