diff --git a/source/core/timed-out.ts b/source/core/timed-out.ts index 9cdd79c04..cede9e5db 100644 --- a/source/core/timed-out.ts +++ b/source/core/timed-out.ts @@ -51,6 +51,7 @@ export default function timedOut(request: ClientRequest, delays: Delays, options request[reentry] = true; const cancelers: Array = []; const {once, unhandleAll} = unhandler(); + const handled: Map = new Map(); const addTimeout = (delay: number, callback: (delay: number, event: string) => void, event: string): (typeof noop) => { const timeout = setTimeout(callback, delay, delay, event) as unknown as NodeJS.Timeout; @@ -58,6 +59,7 @@ export default function timedOut(request: ClientRequest, delays: Delays, options timeout.unref?.(); const cancel = (): void => { + handled.set(event, true); clearTimeout(timeout); }; @@ -69,7 +71,13 @@ export default function timedOut(request: ClientRequest, delays: Delays, options const {host, hostname} = options; const timeoutHandler = (delay: number, event: string): void => { - request.destroy(new TimeoutError(delay, event)); + // Use setTimeout to allow for any cancelled events to be handled first, + // to prevent firing any TimeoutError unneeded when the event loop is busy or blocked + setTimeout(() => { + if (!handled.has(event)) { + request.destroy(new TimeoutError(delay, event)); + } + }, 0); }; const cancelTimeouts = (): void => { diff --git a/test/retry.ts b/test/retry.ts index c615291d2..25e5dd724 100644 --- a/test/retry.ts +++ b/test/retry.ts @@ -1,8 +1,8 @@ -import process from 'node:process'; import {EventEmitter} from 'node:events'; import {PassThrough as PassThroughStream} from 'node:stream'; import type {Socket} from 'node:net'; import http from 'node:http'; +import https from 'node:https'; import test from 'ava'; import is from '@sindresorhus/is'; import type {Handler} from 'express'; @@ -22,18 +22,16 @@ const handler413: Handler = (_request, response) => { response.end(); }; -const createSocketTimeoutStream = (): http.ClientRequest => { - const stream = new PassThroughStream(); - // @ts-expect-error Mocking the behaviour of a ClientRequest - stream.setTimeout = (ms, callback) => { - process.nextTick(callback); - }; - - // @ts-expect-error Mocking the behaviour of a ClientRequest - stream.abort = () => {}; - stream.resume(); +const createSocketTimeoutStream = (url: string): http.ClientRequest => { + if (url.includes('https:')) { + return https.request(url, { + timeout: 1, + }); + } - return stream as unknown as http.ClientRequest; + return http.request(url, { + timeout: socketTimeout, + }); }; test('works on timeout', withServer, async (t, server, got) => { @@ -57,7 +55,7 @@ test('works on timeout', withServer, async (t, server, got) => { } knocks++; - return createSocketTimeoutStream(); + return createSocketTimeoutStream(server.url); }, })).body, 'who`s there?'); }); @@ -93,7 +91,7 @@ test('setting to `0` disables retrying', async t => { return 0; }, }, - request: () => createSocketTimeoutStream(), + request: () => createSocketTimeoutStream('https://example.com'), }), { instanceOf: TimeoutError, message: `Timeout awaiting 'socket' for ${socketTimeout}ms`, diff --git a/test/timeout.ts b/test/timeout.ts index b48cdb9a1..2146149b8 100644 --- a/test/timeout.ts +++ b/test/timeout.ts @@ -1,8 +1,9 @@ import process from 'node:process'; import {EventEmitter} from 'node:events'; -import stream, {PassThrough as PassThroughStream} from 'node:stream'; +import stream from 'node:stream'; import {pipeline as streamPipeline} from 'node:stream/promises'; import http from 'node:http'; +import https from 'node:https'; import net from 'node:net'; import getStream from 'get-stream'; import test from 'ava'; @@ -90,17 +91,9 @@ test.serial('socket timeout', async t => { limit: 0, }, request() { - const stream = new PassThroughStream(); - // @ts-expect-error Mocking the behaviour of a ClientRequest - stream.setTimeout = (ms, callback) => { - process.nextTick(callback); - }; - - // @ts-expect-error Mocking the behaviour of a ClientRequest - stream.abort = () => {}; - stream.resume(); - - return stream as unknown as http.ClientRequest; + return https.request('https://example.com', { + timeout: 0, + }); }, }), {