1 | import net from 'node:net';
|
2 | import unhandler from './utils/unhandle.js';
|
3 | const reentry = Symbol('reentry');
|
4 | const noop = () => { };
|
5 | export class TimeoutError extends Error {
|
6 | event;
|
7 | code;
|
8 | constructor(threshold, event) {
|
9 | super(`Timeout awaiting '${event}' for ${threshold}ms`);
|
10 | this.event = event;
|
11 | this.name = 'TimeoutError';
|
12 | this.code = 'ETIMEDOUT';
|
13 | }
|
14 | }
|
15 | export default function timedOut(request, delays, options) {
|
16 | if (reentry in request) {
|
17 | return noop;
|
18 | }
|
19 | request[reentry] = true;
|
20 | const cancelers = [];
|
21 | const { once, unhandleAll } = unhandler();
|
22 | const addTimeout = (delay, callback, event) => {
|
23 | const timeout = setTimeout(callback, delay, delay, event);
|
24 | timeout.unref?.();
|
25 | const cancel = () => {
|
26 | clearTimeout(timeout);
|
27 | };
|
28 | cancelers.push(cancel);
|
29 | return cancel;
|
30 | };
|
31 | const { host, hostname } = options;
|
32 | const timeoutHandler = (delay, event) => {
|
33 | request.destroy(new TimeoutError(delay, event));
|
34 | };
|
35 | const cancelTimeouts = () => {
|
36 | for (const cancel of cancelers) {
|
37 | cancel();
|
38 | }
|
39 | unhandleAll();
|
40 | };
|
41 | request.once('error', error => {
|
42 | cancelTimeouts();
|
43 |
|
44 |
|
45 | if (request.listenerCount('error') === 0) {
|
46 | throw error;
|
47 | }
|
48 | });
|
49 | if (delays.request !== undefined) {
|
50 | const cancelTimeout = addTimeout(delays.request, timeoutHandler, 'request');
|
51 | once(request, 'response', (response) => {
|
52 | once(response, 'end', cancelTimeout);
|
53 | });
|
54 | }
|
55 | if (delays.socket !== undefined) {
|
56 | const { socket } = delays;
|
57 | const socketTimeoutHandler = () => {
|
58 | timeoutHandler(socket, 'socket');
|
59 | };
|
60 | request.setTimeout(socket, socketTimeoutHandler);
|
61 |
|
62 |
|
63 |
|
64 | cancelers.push(() => {
|
65 | request.removeListener('timeout', socketTimeoutHandler);
|
66 | });
|
67 | }
|
68 | const hasLookup = delays.lookup !== undefined;
|
69 | const hasConnect = delays.connect !== undefined;
|
70 | const hasSecureConnect = delays.secureConnect !== undefined;
|
71 | const hasSend = delays.send !== undefined;
|
72 | if (hasLookup || hasConnect || hasSecureConnect || hasSend) {
|
73 | once(request, 'socket', (socket) => {
|
74 | const { socketPath } = request;
|
75 |
|
76 | if (socket.connecting) {
|
77 | const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0);
|
78 | if (hasLookup && !hasPath && socket.address().address === undefined) {
|
79 | const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
|
80 | once(socket, 'lookup', cancelTimeout);
|
81 | }
|
82 | if (hasConnect) {
|
83 | const timeConnect = () => addTimeout(delays.connect, timeoutHandler, 'connect');
|
84 | if (hasPath) {
|
85 | once(socket, 'connect', timeConnect());
|
86 | }
|
87 | else {
|
88 | once(socket, 'lookup', (error) => {
|
89 | if (error === null) {
|
90 | once(socket, 'connect', timeConnect());
|
91 | }
|
92 | });
|
93 | }
|
94 | }
|
95 | if (hasSecureConnect && options.protocol === 'https:') {
|
96 | once(socket, 'connect', () => {
|
97 | const cancelTimeout = addTimeout(delays.secureConnect, timeoutHandler, 'secureConnect');
|
98 | once(socket, 'secureConnect', cancelTimeout);
|
99 | });
|
100 | }
|
101 | }
|
102 | if (hasSend) {
|
103 | const timeRequest = () => addTimeout(delays.send, timeoutHandler, 'send');
|
104 |
|
105 | if (socket.connecting) {
|
106 | once(socket, 'connect', () => {
|
107 | once(request, 'upload-complete', timeRequest());
|
108 | });
|
109 | }
|
110 | else {
|
111 | once(request, 'upload-complete', timeRequest());
|
112 | }
|
113 | }
|
114 | });
|
115 | }
|
116 | if (delays.response !== undefined) {
|
117 | once(request, 'upload-complete', () => {
|
118 | const cancelTimeout = addTimeout(delays.response, timeoutHandler, 'response');
|
119 | once(request, 'response', cancelTimeout);
|
120 | });
|
121 | }
|
122 | if (delays.read !== undefined) {
|
123 | once(request, 'response', (response) => {
|
124 | const cancelTimeout = addTimeout(delays.read, timeoutHandler, 'read');
|
125 | once(response, 'end', cancelTimeout);
|
126 | });
|
127 | }
|
128 | return cancelTimeouts;
|
129 | }
|