Skip to content

fix(mobile): 延迟压缩头提交后不再二次写头 - #165

Closed
mexiaosqwq wants to merge 1 commit into
DSH-APP:mainfrom
mexiaosqwq:codex/fix-mobile-compress-implicit-writehead
Closed

mexiaosqwq wants to merge 1 commit into
DSH-APP:mainfrom
mexiaosqwq:codex/fix-mobile-compress-implicit-writehead

Conversation

@mexiaosqwq

Copy link
Copy Markdown

问题

宿主 webserver 开启 compression: gzip(默认 1024B 阈值)时,内置移动插件对「小 JSON + Content-Length」响应抛 ERR_HTTP_HEADERS_SENT,客户端表现为 fetch failed / ECONNRESET(例如 claude-style 的 sendJson 接口,以及本插件自己的会话删除 JSON 错误响应)。

根因

tools/mobile-server/compress.js 的 patchedWriteHead() 先调用 flush(this) 用 origWriteHead 提交延迟头;随后 this.headersSent 已为 true,但仍落到 return origWriteHead.apply(this, args),同一次响应第二次写头。

触发路径:

  1. 宿主 compression 的 on-headers 包装把 writeHead 的头部并入响应头表,只把 statusCode 传给下一层;
  2. body 低于宿主压缩阈值时,宿主不写 Content-Encoding,插件据 res.getHeaders() 仍判定可以延迟;
  3. res.end() 时宿主 compression 的 end 代理补一次隐式 writeHead(statusCode);
  4. 插件 flush() 已把延迟头发出,随后再次 origWriteHead → ERR_HTTP_HEADERS_SENT;
  5. 宿主 dispatch 捕获后 res.destroy(),客户端连接中断。

≥1024B 的响应由宿主压缩接管(Content-Encoding 已设置,插件不延迟),没有 Content-Length 的分块响应同样由宿主处理,所以只有「小 JSON + Content-Length」这一类路径命中。

改动

  • tools/mobile-server/compress.js:flush() 返回是否提交了延迟头;patchedWriteHead() 在该调用提交后遇到纯状态码重入时直接返回,等同 Node _implicitHeader 在 _header 已设置时的 no-op。带 headers 的显式第二次 writeHead 仍按原生抛错。
  • app/src/main/assets/builtin-plugins/dsh-web-mobile/lib/compress.js 与 .map:由 node tools/apply-mobile-client-patches.mjs --server tools/mobile-plugin-upstream/lib --write 重建。
  • tools/test-mobile-server-streams.mjs:新增回归用例,按宿主 gzip 的包装形状(头部并入响应头表、end 时隐式写头)复现该组合。

验证

  • 修复前新用例失败:ERR_HTTP_HEADERS_SENT: Cannot write headers after they are sent to the client;修复后通过。
  • node --test tools/test-mobile-server-streams.mjs:35/35 通过。
  • DSHA_MOBILE_UPSTREAM_DIR=tools/mobile-plugin-upstream/lib node --test tools/test-mobile-server-patches.mjs:3/3 通过(canonical 产物、部署产物与 source map 一致)。
  • 另用宿主同款 compression@1.8.2 接线隔离验证:小 JSON + Content-Length 返回 200、Content-Length: 121、正文逐字节一致;medium/large 仍由宿主 gzip 压缩。

备注

宿主 webserver 开启 compression: gzip 时,patchedWriteHead 在 flush(this)
提交延迟头之后,this.headersSent 已为 true 却仍把这次状态码重入交给原生
writeHead,同一次响应出现第二次写头,抛 ERR_HTTP_HEADERS_SENT;宿主
dispatch 捕获后 res.destroy(),客户端表现为 fetch failed / ECONNRESET。

flush() 改为返回是否提交了延迟头;patchedWriteHead 在该调用提交后遇到纯
状态码重入时直接返回,等同 Node _implicitHeader 在 _header 已设置时的
no-op。带 headers 的显式第二次 writeHead 仍按原生抛错。

重建 lib/compress.js 与 lib/compress.js.map,并在
test-mobile-server-streams.mjs 增加按宿主 gzip 包装形状复现的回归用例。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants