UNPKG

5.01 kBJavaScriptView Raw
1import net from 'node:net';
2import unhandler from './utils/unhandle.js';
3const reentry = Symbol('reentry');
4const noop = () => { };
5export 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}
15export 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 // Save original behavior
44 /* istanbul ignore next */
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 // `request.setTimeout(0)` causes a memory leak.
62 // We can just remove the listener and forget about the timer - it's unreffed.
63 // See https://github.com/sindresorhus/got/issues/690
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 /* istanbul ignore next: hard to test */
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 /* istanbul ignore next: hard to test */
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}