
gzip 的 end 为什么不能证明响应发完?
gzip 是可读流,res 是 HTTP 响应可写流。gzip 发出 end,说明压缩器的可读端没有更多数据;通过 pipe(res) 连接时,Node 会在源流结束后调用目标的 end()。但目标写入还可能留在待处理队列,响应此时未必已经发完。仅监听压缩流的 end 就释放关联资源,或者向上游报告请求完成,时序上都早了一步。一个信号属于压缩器,一个信号属于响应对象,名字相近不代表生命周期相同。看 Node 流时,我会先确认事件发出者是谁,再决定后续动作挂在哪里。
response.finish 到底说明什么?
ServerResponse 的 finish 表示最后一段响应数据已交给底层系统处理,并不表示客户端已经收到或解码。若业务逻辑关心本机响应流是否完成,观察 res 的 finish;若还要判断对端是否读完,需要客户端侧证据,服务端的这个事件不能替代它。异常路径也要单独处理,error、连接提前关闭并不会变成一次正常 finish。比如日志标记"响应已发送",用 finish 比压缩器 end 更贴近它要表达的含义;若记录的是"压缩输出已生成",则应当明确它只描述源端。
什么时候才拿得到完整 gzip 数据?
gzip 尾部校验数据也属于压缩结果的一部分。要在内存里取得完整压缩数据,应该收集完整输出或使用 pipeline,等待它成功完成后再使用 buffer;中途读到的 chunk 不能被当成完整 gzip 文件。写入文件时同理,等目标写入完成并检查错误,再把文件交给下一步。pipeline 会把流接起来并在完成或失败时结束 Promise,适合把完成条件放在一个地方处理。这里只建议用它表达整条链的完成,不要把 Promise 成功和远端应用已经消费数据混为一谈。
本机实验能证明什么?
tools/verify-node-gzip-buffer.mjs 开了一个 loopback HTTP 服务,让 createGzip() 直接 pipe(res)。脚本用 Node 的 http.get 收原始响应体,避免高层客户端自动解压;随后用 gunzipSync 还原,并逐字节和输入比较。运行得到 gzip:end < response:finish < client:end,状态码是 200,编码头是 gzip,收到 66 字节,解压内容与输入相等。这个结果证明这次运行的事件顺序和完整性,不证明所有负载下事件间隔必然很长,也没有模拟慢客户端。特意使用原始 HTTP 客户端,是因为 fetch 会按内容编码自动解压,拿到的 buffer 已不是网络响应中的 gzip 字节。误用自动解压后的内容再交给 gunzipSync,测到的会是客户端处理行为,而非压缩流实际写出的字节。
node tools/verify-node-gzip-buffer.mjs
status=200
content-encoding=gzip
event-order=gzip:end < response:finish < client:end
compressed-bytes=66
decoded-equals-input=true
flush 问题与 HTTP 响应有什么不同?
站内已有文章讨论流结束时残留缓冲的 flush 保证;这里测的是 HTTP pipe 连接里压缩源的 end、响应的 finish、客户端读完各自处于什么位置。它们确实不同,不能把源流完成误读成请求端已经收齐。碰到接口要把压缩结果再存档、计算摘要或转交下游的情况,会等完整管线成功,而不会从 gzip:end 猜目标端也结束了。这个边界看起来细,写日志、确认上传完成或触发下一步任务时却很实际:完成条件要跟数据的最终去向对齐,不能因为上游先发了事件就提前释放下游仍在使用的东西。客户端收到完整响应后,才有读取和解压这些字节的条件。