Skip to content

fix: avoid spurious ERR_STREAM_PREMATURE_CLOSE on Node 24.17.0+ by declining gzip for API requests#1516

Closed
sakkadak wants to merge 1 commit into
danger:mainfrom
sakkadak:fix/node24-premature-close
Closed

fix: avoid spurious ERR_STREAM_PREMATURE_CLOSE on Node 24.17.0+ by declining gzip for API requests#1516
sakkadak wants to merge 1 commit into
danger:mainfrom
sakkadak:fix/node24-premature-close

Conversation

@sakkadak

Copy link
Copy Markdown

Fixes #1515

Problem

On Node 24.17.0+, every danger ci run fails during DSL assembly while fetching the PR files / diff / commits, before the Dangerfile is evaluated:

FetchError: Invalid response body ... /pulls/<n>/files: Premature close
  at Gunzip.<anonymous> (node_modules/node-fetch/lib/index.js:400)
  errno: 'ERR_STREAM_PREMATURE_CLOSE', code: 'ERR_STREAM_PREMATURE_CLOSE'

Deterministic: green on Node 24.16.0, red on Node 24.17.0.

Root cause

Node 24.17.0's http.Agent change leaves a 'data' listener on idle keep-alive sockets (nodejs/node#63989; fixed in nodejs/node#64004 "http: avoid stream listeners on idle agent sockets"). node-fetch@2's fixResponseChunkedTransferBadEnding (lib/index.js:1739/1745) inspects socket.listenerCount('data') to detect an unclean end, and now false-positives for Transfer-Encoding: chunked responses with no Content-Length — i.e. gzip-encoded GitHub API responses — throwing a synthetic ERR_STREAM_PREMATURE_CLOSE. It is not a real decode error; the Gunzip in the trace is just the body stream the synthetic error is destroyed onto.

node-fetch@2 is frozen and node-fetch@3 is ESM-only, and the Node fix isn't in a released line yet, so the practical place to address this is Danger's own fetch wrapper.

Fix

Set compress: false (guarded with === undefined, so callers can still opt in) in the shared api() wrapper, alongside the existing proxy-agent handling. The server then returns identity-encoded bodies with a Content-Length, so node-fetch's bad-ending check never arms. This mirrors how the wrapper already centralizes cross-cutting request config (the proxy agent).

Backwards compatibility

  • No signature / dependency / Node-floor change; valid on all supported Node versions.
  • The === undefined guard preserves any explicit caller compress setting.
  • Only observable change: requests no longer advertise gzip; responses come back identity + Content-Length. Danger's payloads are small JSON, so the bandwidth difference is negligible.

Tests

Extends source/api/_tests/fetch.test.ts (same TestServer pattern as the proxy-agent tests):

  • defaults compress to false
  • does not override an explicitly provided compress

yarn jest source/api/_tests/fetch.test.ts → 9/9 pass. tsc -p tsconfig.production.json, eslint, and prettier --check are all clean.

Notes / open to feedback

  • This applies to every platform that goes through api() (GitHub / GitLab / BitBucket), not just GitHub. The bug is platform-agnostic, so the broad fix protects them all — but I'm happy to narrow the scope to the GitHub path if you'd prefer.
  • This is an interim shim; it can be reverted once a patched Node line ships or feat: swapping our node-fetch for unidici / native #1514 (undici migration) lands.

Opened as a draft for your review of the approach before marking ready.

… 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) <noreply@anthropic.com>
@sakkadak sakkadak marked this pull request as ready for review June 23, 2026 15:27
@sakkadak

Copy link
Copy Markdown
Author

Upstream Node status (for context): the Node-side fix (nodejs/node#64004) is merged to main and backported to the v24.x-staging / v22.x-staging branches — but it is not in any released tag, not on the v24.x release branch, and not in the current v24.18.0 release proposal (#64062) yet. So it hasn't been promoted off staging into a release.

Net: anyone on the current secure releases (24.17.0 / 22.23.0) — which are effectively mandatory due to CVE-2026-48931 — has no Node-level fix available today, which is the gap this change covers. It can be reverted once a tagged release actually carries the backport.

charlesBochet added a commit to twentyhq/twenty that referenced this pull request Jun 25, 2026
…OSE on Node 24 (#22171)

## Problem

The `danger-js` check (`twenty-utils:danger:ci`) started failing
intermittently with:

```
FetchError: Invalid response body while trying to fetch
https://api.github.com/repos/twentyhq/twenty/pulls/<n>/files: Premature close
  errno: 'ERR_STREAM_PREMATURE_CLOSE'
```

It fails before the Dangerfile even runs, while fetching PR files / diff
/ commits. The existing retry wrapper
([#22151](#22151)) reduced it but
can't absorb longer GitHub-API windows, so checks still go red.

## Root cause

Not "node-fetch is old" generically — a specific recent regression:

- Node **22.23.0 / 24.17.0** shipped a security fix for CVE-2026-48931
(http.Agent response-queue poisoning) that attaches a `'data'` listener
to idle keep-alive sockets.
- `node-fetch@2` misreads that listener as an unclean connection close —
but only on **gzip-encoded responses without `Content-Length`**, which
is exactly what `api.github.com` returns.
- The GitHub-hosted runners rolling into the patched Node 24.17.x in
recent weeks is why this surfaced now.

See
[danger/danger-js#1515](danger/danger-js#1515),
[nodejs/node#63989](nodejs/node#63989).

## Why this approach

- `node-fetch@2` can't be removed downstream — Danger imports it
directly, and it's pervasive transitively (gaxios/googleapis). Dropping
it is an upstream migration.
- We don't want to pin an old Node version.

So: bump `danger` 13.0.4 → 13.0.8 and backport
[danger/danger-js#1516](danger/danger-js#1516)
via a yarn patch — set `compress: false` on Danger's shared `api()`
wrapper. GitHub then returns identity-encoded responses with
`Content-Length`, and node-fetch's faulty premature-close detector never
fires. Negligible bandwidth cost on these small JSON payloads; explicit
caller overrides are preserved via an `=== undefined` guard.

## Changes

- `packages/twenty-utils/package.json` — `danger` → patched 13.0.8
- `yarn.lock` — registers the `danger@patch:` resolution
- `.yarn/patches/danger-npm-13.0.8-48aba2788c.patch` — the `compress:
false` fix

## Verification

- Patch dry-run applies cleanly against pristine danger 13.0.8 source.
- Inspected yarn's materialized patched cache package — the `compress`
fix is present in the linked `distribution/api/fetch.js`.
- Confirmed the failing calls (`getPullRequestInfo` /
`getPullRequestCommits` / `getPullRequestDiff`) all route through
`this.api` → the patched wrapper.

## Lifecycle

Temporary backport. When #1516 ships in a Danger release, drop the patch
and bump to that version (flagged in a comment inside the patch). The
existing CI retry wrapper stays as defense-in-depth.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22171?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
@sakkadak

Copy link
Copy Markdown
Author

Superseded by #1514 (released in v13.0.10), which removes node-fetch in favor of undici and fixes this at the root — so this compress:false shim no longer applies (the file it patched is now undici-based). Closing in favor of that. Thanks!

@sakkadak sakkadak closed this Jun 29, 2026
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.

danger ci fails with FetchError: Premature close (ERR_STREAM_PREMATURE_CLOSE) on Node 24.17.0+

1 participant