fix: retry refused connections on Node.js 24.20+

Since Node.js 24.20 (nodejs/node#64847) the ClientRequest end() callback
fires with the connection error after the request has already emitted
error and been destroyed. That late ERR_SOCKET_CLOSED_BEFORE_CONNECTION was
forwarded to _final, destroying the stream and cancelling the retry that the
error handler had scheduled, so ECONNREFUSED was no longer retried and the
generic error surfaced instead of the real one.

Ignore the end() callback error once the request is destroyed; the real
error already drives the retry state machine. Write errors like EPIPE that
arrive while the request is still alive keep reaching the retry classifier.

Fixes #2469
This commit is contained in:
dualfroz committed 2026-08-28 03:35:54 +02:00
1 parent 6c46990d75
commit 017b847750
2 files changed
+53 -3

No files matched your search

+43 -2
View File
@@ -1,6 +1,6 @@
import {EventEmitter} from 'node:events';
import {PassThrough as PassThroughStream} from 'node:stream';
import type {Socket} from 'node:net';
import type {AddressInfo, Socket} from 'node:net';
import http from 'node:http';
import https from 'node:https';
import test from 'ava';
@@ -8,7 +8,7 @@ import is from '@sindresorhus/is';
import type {Handler} from 'express';
import getStream from 'get-stream';
import {pEvent} from 'p-event';
import got, {HTTPError, TimeoutError} from '../source/index.js';
import got, {HTTPError, RequestError, TimeoutError} from '../source/index.js';
import type Request from '../source/core/index.js';
import withServer from './helpers/with-server.js';
@@ -1372,3 +1372,44 @@ test('does not retry after promise settles (issue #1489)', async t => {
t.is(response.statusCode, 200);
t.false(retryTriggered);
});
test('retries on a refused connection and rejects with the real ECONNREFUSED', async t => {
// Regression test for https://github.com/sindresorhus/got/issues/2469
// Since Node.js 24.20 (nodejs/node#64847) the `request.end()` callback is
// invoked with the connection-phase error. Before the fix that error
// (surfaced as ERR_SOCKET_CLOSED_BEFORE_CONNECTION) terminated the request
// after a single attempt, ignoring `retry` and masking the real error.
const server = http.createServer();
await new Promise<void>(resolve => {
server.listen(0, '127.0.0.1', resolve);
});
const {port} = server.address() as AddressInfo;
await new Promise<void>(resolve => {
server.close(() => {
resolve();
});
});
let retries = 0;
const error = await t.throwsAsync<RequestError>(got(`http://127.0.0.1:${port}`, {
retry: {
limit: 2,
calculateDelay: () => 1,
},
hooks: {
beforeRetry: [
() => {
retries++;
},
],
},
}), {
instanceOf: RequestError,
});
t.is(error?.code, 'ECONNREFUSED');
t.is(retries, 2);
});