Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<!-- Your comment below this -->

- 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]

<!-- Your comment above this -->

Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions source/api/_tests/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,20 @@ describe("fetch", () => {
let agent = options.agent as HttpProxyAgent<string>
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)
})
})
13 changes: 13 additions & 0 deletions source/api/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading