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:
1 parent
6c46990d75
commit
017b847750
2 files changed
+53
-3
No files matched your search
+43
-2
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user