From b4751fe868dadb9589fa4df84e99848bbee01c90 Mon Sep 17 00:00:00 2001 From: Sakkada Khun Date: Tue, 23 Jun 2026 11:03:19 -0400 Subject: [PATCH] fix: avoid bogus Premature close on Node 24.17+ by declining gzip for API requests Node >= 24.17.0 attaches a 'data' listener to idle keep-alive sockets in the http.Agent free pool (the CVE-2026-48931 "response queue poisoning" hardening). node-fetch@2's fixResponseChunkedTransferBadEnding reads socket.listenerCount('data') and misreads that listener as an unclean close, throwing a spurious ERR_STREAM_PREMATURE_CLOSE for chunked responses with no Content-Length (i.e. gzip-encoded GitHub API responses). This breaks every `danger ci` run on Node 24.17.0 while fetching PR files/diff/commits. Declining gzip (compress:false) makes the server return identity-encoded bodies with a Content-Length, so node-fetch's detector never arms. Guarded with === undefined so callers can still opt back into compression. Refs nodejs/node#63989 (report), nodejs/node#64004 (fix). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 ++ source/api/_tests/fetch.test.ts | 16 ++++++++++++++++ source/api/fetch.ts | 13 +++++++++++++ 3 files changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f6c9d54..55f739d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ +- Fix spurious `Premature close` (`ERR_STREAM_PREMATURE_CLOSE`) failures when running on Node 24.17.0+ by no longer requesting gzip-compressed GitHub API responses - [@sakkadak] @@ -2137,6 +2138,7 @@ Not usable for others, only stubs of classes etc. - [@orta] [@rouby]: https://github.com/rouby [@rzgry]: https://github.com/rzgry [@sajjadzamani]: https://github.com/sajjadzamani +[@sakkadak]: https://github.com/sakkadak [@sandratatarevicova]: https://github.com/sandratatarevicova [@sebinsua]: https://github.com/sebinsua [@sgtcoolguy]: https://github.com/sgtcoolguy diff --git a/source/api/_tests/fetch.test.ts b/source/api/_tests/fetch.test.ts index 5a6fa8294..50a586888 100644 --- a/source/api/_tests/fetch.test.ts +++ b/source/api/_tests/fetch.test.ts @@ -172,4 +172,20 @@ describe("fetch", () => { let agent = options.agent as HttpProxyAgent expect(agent["proxy"].href).toBe(proxyUrl) }) + + it("disables compression by default to avoid the Node 24.17+ node-fetch premature-close", async () => { + await server.start({}) + + let options: node_fetch.RequestInit = {} + await api(url, options, true) + expect(options.compress).toBe(false) + }) + + it("does not override an explicitly provided compress option", async () => { + await server.start({}) + + let options: node_fetch.RequestInit = { compress: true } + await api(url, options, true) + expect(options.compress).toBe(true) + }) }) diff --git a/source/api/fetch.ts b/source/api/fetch.ts index 7913d85d6..486160f5a 100644 --- a/source/api/fetch.ts +++ b/source/api/fetch.ts @@ -116,6 +116,19 @@ export function api( init.agent = secure ? new HttpsProxyAgent(proxy) : new HttpProxyAgent(proxy) } + // Node >= 24.17.0 attaches a 'data' listener to idle keep-alive sockets in the http.Agent + // free pool (the CVE-2026-48931 "response queue poisoning" hardening). node-fetch@2's + // chunked-bad-ending detector (fixResponseChunkedTransferBadEnding) reads + // `socket.listenerCount('data')` and misreads that listener as an unclean connection close, + // throwing a bogus `ERR_STREAM_PREMATURE_CLOSE` ("Premature close") for responses sent as + // `Transfer-Encoding: chunked` with no `Content-Length` — i.e. gzip-encoded GitHub API + // responses. Declining gzip makes the server return an identity-encoded body with a + // `Content-Length`, so the detector never arms. Guarded with `=== undefined` so callers can + // still opt back into compression. See nodejs/node#63989 (report) and nodejs/node#64004 (fix). + if (init.compress === undefined) { + init.compress = false + } + return retryableFetch(url, init).then(async (response: node_fetch.Response) => { // Handle failing errors if (!suppressErrorReporting && !response.ok) {