161 lines
4.3 KiB
JavaScript
161 lines
4.3 KiB
JavaScript
|
'use strict';
|
||
|
const net = require('net');
|
||
|
|
||
|
class TimeoutError extends Error {
|
||
|
constructor(threshold, event) {
|
||
|
super(`Timeout awaiting '${event}' for ${threshold}ms`);
|
||
|
this.name = 'TimeoutError';
|
||
|
this.code = 'ETIMEDOUT';
|
||
|
this.event = event;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const reentry = Symbol('reentry');
|
||
|
|
||
|
const noop = () => {};
|
||
|
|
||
|
module.exports = (request, delays, options) => {
|
||
|
/* istanbul ignore next: this makes sure timed-out isn't called twice */
|
||
|
if (request[reentry]) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
request[reentry] = true;
|
||
|
|
||
|
let stopNewTimeouts = false;
|
||
|
|
||
|
const addTimeout = (delay, callback, ...args) => {
|
||
|
// An error had been thrown before. Going further would result in uncaught errors.
|
||
|
// See https://github.com/sindresorhus/got/issues/631#issuecomment-435675051
|
||
|
if (stopNewTimeouts) {
|
||
|
return noop;
|
||
|
}
|
||
|
|
||
|
// Event loop order is timers, poll, immediates.
|
||
|
// The timed event may emit during the current tick poll phase, so
|
||
|
// defer calling the handler until the poll phase completes.
|
||
|
let immediate;
|
||
|
const timeout = setTimeout(() => {
|
||
|
immediate = setImmediate(callback, delay, ...args);
|
||
|
/* istanbul ignore next: added in node v9.7.0 */
|
||
|
if (immediate.unref) {
|
||
|
immediate.unref();
|
||
|
}
|
||
|
}, delay);
|
||
|
|
||
|
/* istanbul ignore next: in order to support electron renderer */
|
||
|
if (timeout.unref) {
|
||
|
timeout.unref();
|
||
|
}
|
||
|
|
||
|
const cancel = () => {
|
||
|
clearTimeout(timeout);
|
||
|
clearImmediate(immediate);
|
||
|
};
|
||
|
|
||
|
cancelers.push(cancel);
|
||
|
|
||
|
return cancel;
|
||
|
};
|
||
|
|
||
|
const {host, hostname} = options;
|
||
|
const timeoutHandler = (delay, event) => {
|
||
|
request.emit('error', new TimeoutError(delay, event));
|
||
|
request.once('error', () => {}); // Ignore the `socket hung up` error made by request.abort()
|
||
|
|
||
|
request.abort();
|
||
|
};
|
||
|
|
||
|
const cancelers = [];
|
||
|
const cancelTimeouts = () => {
|
||
|
stopNewTimeouts = true;
|
||
|
cancelers.forEach(cancelTimeout => cancelTimeout());
|
||
|
};
|
||
|
|
||
|
request.once('error', cancelTimeouts);
|
||
|
request.once('response', response => {
|
||
|
response.once('end', cancelTimeouts);
|
||
|
});
|
||
|
|
||
|
if (delays.request !== undefined) {
|
||
|
addTimeout(delays.request, timeoutHandler, 'request');
|
||
|
}
|
||
|
|
||
|
if (delays.socket !== undefined) {
|
||
|
const socketTimeoutHandler = () => {
|
||
|
timeoutHandler(delays.socket, 'socket');
|
||
|
};
|
||
|
|
||
|
request.setTimeout(delays.socket, socketTimeoutHandler);
|
||
|
|
||
|
// `request.setTimeout(0)` causes a memory leak.
|
||
|
// We can just remove the listener and forget about the timer - it's unreffed.
|
||
|
// See https://github.com/sindresorhus/got/issues/690
|
||
|
cancelers.push(() => request.removeListener('timeout', socketTimeoutHandler));
|
||
|
}
|
||
|
|
||
|
if (delays.lookup !== undefined && !request.socketPath && !net.isIP(hostname || host)) {
|
||
|
request.once('socket', socket => {
|
||
|
/* istanbul ignore next: hard to test */
|
||
|
if (socket.connecting) {
|
||
|
const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
|
||
|
socket.once('lookup', cancelTimeout);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (delays.connect !== undefined) {
|
||
|
request.once('socket', socket => {
|
||
|
/* istanbul ignore next: hard to test */
|
||
|
if (socket.connecting) {
|
||
|
const timeConnect = () => addTimeout(delays.connect, timeoutHandler, 'connect');
|
||
|
|
||
|
if (request.socketPath || net.isIP(hostname || host)) {
|
||
|
socket.once('connect', timeConnect());
|
||
|
} else {
|
||
|
socket.once('lookup', error => {
|
||
|
if (error === null) {
|
||
|
socket.once('connect', timeConnect());
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (delays.secureConnect !== undefined && options.protocol === 'https:') {
|
||
|
request.once('socket', socket => {
|
||
|
/* istanbul ignore next: hard to test */
|
||
|
if (socket.connecting) {
|
||
|
socket.once('connect', () => {
|
||
|
const cancelTimeout = addTimeout(delays.secureConnect, timeoutHandler, 'secureConnect');
|
||
|
socket.once('secureConnect', cancelTimeout);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (delays.send !== undefined) {
|
||
|
request.once('socket', socket => {
|
||
|
const timeRequest = () => addTimeout(delays.send, timeoutHandler, 'send');
|
||
|
/* istanbul ignore next: hard to test */
|
||
|
if (socket.connecting) {
|
||
|
socket.once('connect', () => {
|
||
|
request.once('upload-complete', timeRequest());
|
||
|
});
|
||
|
} else {
|
||
|
request.once('upload-complete', timeRequest());
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (delays.response !== undefined) {
|
||
|
request.once('upload-complete', () => {
|
||
|
const cancelTimeout = addTimeout(delays.response, timeoutHandler, 'response');
|
||
|
request.once('response', cancelTimeout);
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
module.exports.TimeoutError = TimeoutError;
|