UNPKG

4.07 kBJavaScriptView Raw
1'use strict';
2const net = require('net');
3
4class TimeoutError extends Error {
5 constructor(threshold, event) {
6 super(`Timeout awaiting '${event}' for ${threshold}ms`);
7 this.name = 'TimeoutError';
8 this.code = 'ETIMEDOUT';
9 this.event = event;
10 }
11}
12
13const reentry = Symbol('reentry');
14
15function addTimeout(delay, callback, ...args) {
16 // Event loop order is timers, poll, immediates.
17 // The timed event may emit during the current tick poll phase, so
18 // defer calling the handler until the poll phase completes.
19 let immediate;
20 const timeout = setTimeout(() => {
21 immediate = setImmediate(callback, delay, ...args);
22 /* istanbul ignore next: added in node v9.7.0 */
23 if (immediate.unref) {
24 immediate.unref();
25 }
26 }, delay);
27
28 /* istanbul ignore next: in order to support electron renderer */
29 if (timeout.unref) {
30 timeout.unref();
31 }
32
33 const cancel = () => {
34 clearTimeout(timeout);
35 clearImmediate(immediate);
36 };
37
38 return cancel;
39}
40
41module.exports = (request, delays, options) => {
42 /* istanbul ignore next: this makes sure timed-out isn't called twice */
43 if (request[reentry]) {
44 return;
45 }
46
47 request[reentry] = true;
48 const {host, hostname} = options;
49 const timeoutHandler = (delay, event) => {
50 request.emit('error', new TimeoutError(delay, event));
51 request.once('error', () => {}); // Ignore the `socket hung up` error made by request.abort()
52
53 request.abort();
54 };
55
56 const cancelers = [];
57 const cancelTimeouts = () => {
58 cancelers.forEach(cancelTimeout => cancelTimeout());
59 };
60
61 request.once('error', cancelTimeouts);
62 request.once('response', response => {
63 response.once('end', cancelTimeouts);
64 });
65
66 if (delays.request !== undefined) {
67 const cancelTimeout = addTimeout(delays.request, timeoutHandler, 'request');
68 cancelers.push(cancelTimeout);
69 }
70
71 if (delays.socket !== undefined) {
72 request.setTimeout(delays.socket, () => {
73 timeoutHandler(delays.socket, 'socket');
74 });
75 }
76
77 if (delays.lookup !== undefined && !request.socketPath && !net.isIP(hostname || host)) {
78 request.once('socket', socket => {
79 /* istanbul ignore next: hard to test */
80 if (socket.connecting) {
81 const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
82 cancelers.push(cancelTimeout);
83 socket.once('lookup', cancelTimeout);
84 }
85 });
86 }
87
88 if (delays.connect !== undefined) {
89 request.once('socket', socket => {
90 /* istanbul ignore next: hard to test */
91 if (socket.connecting) {
92 const timeConnect = () => {
93 const cancelTimeout = addTimeout(delays.connect, timeoutHandler, 'connect');
94 cancelers.push(cancelTimeout);
95 return cancelTimeout;
96 };
97
98 if (request.socketPath || net.isIP(hostname || host)) {
99 socket.once('connect', timeConnect());
100 } else {
101 socket.once('lookup', () => {
102 socket.once('connect', timeConnect());
103 });
104 }
105 }
106 });
107 }
108
109 if (delays.secureConnect !== undefined && options.protocol === 'https:') {
110 request.once('socket', socket => {
111 /* istanbul ignore next: hard to test */
112 if (socket.connecting) {
113 socket.once('connect', () => {
114 const cancelTimeout = addTimeout(delays.secureConnect, timeoutHandler, 'secureConnect');
115 cancelers.push(cancelTimeout);
116 socket.once('secureConnect', cancelTimeout);
117 });
118 }
119 });
120 }
121
122 if (delays.send !== undefined) {
123 request.once('socket', socket => {
124 const timeRequest = () => {
125 const cancelTimeout = addTimeout(delays.send, timeoutHandler, 'send');
126 cancelers.push(cancelTimeout);
127 return cancelTimeout;
128 };
129 /* istanbul ignore next: hard to test */
130 if (socket.connecting) {
131 socket.once('connect', () => {
132 request.once('upload-complete', timeRequest());
133 });
134 } else {
135 request.once('upload-complete', timeRequest());
136 }
137 });
138 }
139
140 if (delays.response !== undefined) {
141 request.once('upload-complete', () => {
142 const cancelTimeout = addTimeout(delays.response, timeoutHandler, 'response');
143 cancelers.push(cancelTimeout);
144 request.once('response', cancelTimeout);
145 });
146 }
147};
148
149module.exports.TimeoutError = TimeoutError;