Skip to content

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) #2469

Closed

Description

@BowlesCR

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 direct net.connect() testing on both versions.
  • DNS resolution order for localhost is 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 in got determines the final outcome.

Environment

  • got: reproduced on 11.8.6 and 15.1.0 (latest)
  • Node.js: reproduces on 24.20.0; does not reproduce on 24.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

  1. added 3 commits that reference this issue on Aug 28, 2026
    0d8e629
    9e37015
    abdb1ee
  2. Kikobeats commented on Sep 14, 2026

    @Kikobeats
    Contributor

    Unfortunately, got v11 is also affected, potentially impacting 10x as many users:

    Image

    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?

  3. MikeMcC399 commented on Sep 15, 2026

    @MikeMcC399

    @Kikobeats

    ... there’s no v11 branch to open the PR against. Could you create one from the v11 tag?

    The README says:

    Got v11 is no longer maintained and we will not accept any backport requests.

    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@16

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions