Repository navigation
Description
Title
Retries stop after 1 attempt on connection refusal under Node.js 24.20.0+, surfaced as generic ERR_SOCKET_CLOSED_BEFORE_CONNECTION instead of the real error (regression from nodejs/node#64847)
Body
Describe the bug
Since Node.js 24.20.0, a request against a refused connection (ECONNREFUSED) no longer retries. Instead of the real error, the request rejects immediately after a single attempt with a generic ERR_SOCKET_CLOSED_BEFORE_CONNECTION, and the retry option is effectively ignored.
Reproduces on both [email protected] and current got@latest (15.1.0) — this is not specific to an old version.
Reproduction
import got from 'got';
// nothing listening on this port
await got('http://localhost:59999', { retry: { limit: 2 } });Expected behavior
got makes up to 3 attempts (1 initial + 2 retries) and eventually rejects with the real error:
RequestError: connect ECONNREFUSED 127.0.0.1:59999
code: 'ECONNREFUSED'
This is what happens on Node.js ≤24.19.0.
Actual behavior
Only 1 attempt is made. The promise rejects with:
Error: Socket closed before the connection was established
code: 'ERR_SOCKET_CLOSED_BEFORE_CONNECTION'
attempts counter (instrumented via monkey-patching net.Socket.prototype.connect) confirms only 1 socket is ever created — the retry logic never fires.
With retry.limit set higher, a second issue also surfaces: got's internal retry Timeout callback still fires later (since it appears to have been scheduled before the premature rejection), and since the outer promise has already settled, this throws an uncaught exception and crashes the process:
Error: The `onCancel` handler was attached after the promise settled.
at onCancel (node_modules/p-cancelable/index.js:48:12)
at makeRequest (node_modules/got/dist/source/as-promise/index.js:38:13)
at Request.<anonymous> (node_modules/got/dist/source/as-promise/index.js:144:17)
at Request.wrapper (node:events:639:12)
at Request.emit (node:events:514:28)
at Timeout.retry (node_modules/got/dist/source/core/index.js:1278:30)
This looks related to the long-standing #1489, but that issue predates this specific trigger and was closed without a real fix; this reproduces cleanly and consistently on current got, not intermittently.
Root cause (bisected)
We git-bisected Node.js itself between v24.19.0 (good) and v24.20.0 (bad) and isolated it to a single commit:
nodejs/node#64847 — "http: fix writableFinished and 'finish' after write errors" (merged 2026-08-04, shipped in Node 24.20.0)
That PR fixes a genuine Node bug in lib/_http_outgoing.js: previously, a callback passed to request.end(callback) was wired only to the 'finish' event, so if the underlying socket errored before ever connecting, the callback silently never fired at all. The fix makes end() callbacks behave like stream.Writable's — called with null on success, or with the actual error when the write never flushed.
got passes a callback to the underlying ClientRequest's end() and appears to not have a code path that expects that callback to ever fire with an error during the connecting phase (because on every Node version before 24.20.0, it simply never did). Now that Node correctly invokes it with the connection error, something in got's request/retry state machine treats that callback firing as an immediately terminal failure, short-circuiting past the retry logic and substituting the generic ERR_SOCKET_CLOSED_BEFORE_CONNECTION for the real error.
We traced the actual socket-level events with NODE_DEBUG=net and manual instrumentation and confirmed:
- The underlying
net.Socket'error'/'close'event sequence is byte-identical between Node 24.19.0 and 24.20.0 for the same failed connection attempt. - Node's built-in
autoSelectFamily/Happy-Eyeballs connection racing is unaffected — ruled out via directnet.connect()testing on both versions. - DNS resolution order for
localhostis unchanged between versions. - The only difference is whether
request.end(callback)'s callback now receives the real error (24.20.0) vs. never firing at all (≤24.19.0) — and that changes which code path ingotdetermines the final outcome.
Environment
got: reproduced on11.8.6and15.1.0(latest)- Node.js: reproduces on
24.20.0; does not reproduce on24.19.0(same OS image, only Node's point version differs) - OS: Ubuntu 26.04 (also reproduced in a plain container, not specific to our environment)
Additional context
We hit this via cypress-io/github-action@v7, which bundles [email protected] for its wait-on/start health check — all our E2E workflows using that action broke as soon as a routine base-image rebuild picked up Node 24.20.0. Since the bug reproduces on got@latest too, bumping the bundled version won't be a fix for that downstream consumer until this is addressed here.
Happy to share the Node bisect log / instrumented traces if useful.
Activity
Fix retries on connection errors
Preserve failed upload state during retries
Test connection error type
Kikobeats commentedon Sep 14, 2026
Unfortunately, got v11 is also affected, potentially impacting 10x as many users:
I backported #2470 to v11: Kikobeats@ac68c70
However, there’s no v11 branch to open the PR against. Could you create one from the v11 tag?
MikeMcC399 commentedon Sep 15, 2026
@Kikobeats
The README says:
so it looks like the resolution has to be either pinning Node.js <=24.19.0 (or Node.js 26.x <=26.6.0) or migrating to
got@16update cypress action to v7.4.6
update cypress action to v7.4.6