139 lines
5.2 KiB
Plaintext
139 lines
5.2 KiB
Plaintext
import net from 'node:net';
|
|
import unhandler from './utils/unhandle.js';
|
|
const reentry = Symbol('reentry');
|
|
const noop = () => { };
|
|
export class TimeoutError extends Error {
|
|
constructor(threshold, event) {
|
|
super(`Timeout awaiting '${event}' for ${threshold}ms`);
|
|
Object.defineProperty(this, "event", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: event
|
|
});
|
|
Object.defineProperty(this, "code", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
});
|
|
this.name = 'TimeoutError';
|
|
this.code = 'ETIMEDOUT';
|
|
}
|
|
}
|
|
export default function timedOut(request, delays, options) {
|
|
if (reentry in request) {
|
|
return noop;
|
|
}
|
|
request[reentry] = true;
|
|
const cancelers = [];
|
|
const { once, unhandleAll } = unhandler();
|
|
const addTimeout = (delay, callback, event) => {
|
|
const timeout = setTimeout(callback, delay, delay, event);
|
|
timeout.unref?.();
|
|
const cancel = () => {
|
|
clearTimeout(timeout);
|
|
};
|
|
cancelers.push(cancel);
|
|
return cancel;
|
|
};
|
|
const { host, hostname } = options;
|
|
const timeoutHandler = (delay, event) => {
|
|
request.destroy(new TimeoutError(delay, event));
|
|
};
|
|
const cancelTimeouts = () => {
|
|
for (const cancel of cancelers) {
|
|
cancel();
|
|
}
|
|
unhandleAll();
|
|
};
|
|
request.once('error', error => {
|
|
cancelTimeouts();
|
|
// Save original behavior
|
|
/* istanbul ignore next */
|
|
if (request.listenerCount('error') === 0) {
|
|
throw error;
|
|
}
|
|
});
|
|
if (delays.request !== undefined) {
|
|
const cancelTimeout = addTimeout(delays.request, timeoutHandler, 'request');
|
|
once(request, 'response', (response) => {
|
|
once(response, 'end', cancelTimeout);
|
|
});
|
|
}
|
|
if (delays.socket !== undefined) {
|
|
const { socket } = delays;
|
|
const socketTimeoutHandler = () => {
|
|
timeoutHandler(socket, 'socket');
|
|
};
|
|
request.setTimeout(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);
|
|
});
|
|
}
|
|
const hasLookup = delays.lookup !== undefined;
|
|
const hasConnect = delays.connect !== undefined;
|
|
const hasSecureConnect = delays.secureConnect !== undefined;
|
|
const hasSend = delays.send !== undefined;
|
|
if (hasLookup || hasConnect || hasSecureConnect || hasSend) {
|
|
once(request, 'socket', (socket) => {
|
|
const { socketPath } = request;
|
|
/* istanbul ignore next: hard to test */
|
|
if (socket.connecting) {
|
|
const hasPath = Boolean(socketPath ?? net.isIP(hostname ?? host ?? '') !== 0);
|
|
if (hasLookup && !hasPath && socket.address().address === undefined) {
|
|
const cancelTimeout = addTimeout(delays.lookup, timeoutHandler, 'lookup');
|
|
once(socket, 'lookup', cancelTimeout);
|
|
}
|
|
if (hasConnect) {
|
|
const timeConnect = () => addTimeout(delays.connect, timeoutHandler, 'connect');
|
|
if (hasPath) {
|
|
once(socket, 'connect', timeConnect());
|
|
}
|
|
else {
|
|
once(socket, 'lookup', (error) => {
|
|
if (error === null) {
|
|
once(socket, 'connect', timeConnect());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (hasSecureConnect && options.protocol === 'https:') {
|
|
once(socket, 'connect', () => {
|
|
const cancelTimeout = addTimeout(delays.secureConnect, timeoutHandler, 'secureConnect');
|
|
once(socket, 'secureConnect', cancelTimeout);
|
|
});
|
|
}
|
|
}
|
|
if (hasSend) {
|
|
const timeRequest = () => addTimeout(delays.send, timeoutHandler, 'send');
|
|
/* istanbul ignore next: hard to test */
|
|
if (socket.connecting) {
|
|
once(socket, 'connect', () => {
|
|
once(request, 'upload-complete', timeRequest());
|
|
});
|
|
}
|
|
else {
|
|
once(request, 'upload-complete', timeRequest());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (delays.response !== undefined) {
|
|
once(request, 'upload-complete', () => {
|
|
const cancelTimeout = addTimeout(delays.response, timeoutHandler, 'response');
|
|
once(request, 'response', cancelTimeout);
|
|
});
|
|
}
|
|
if (delays.read !== undefined) {
|
|
once(request, 'response', (response) => {
|
|
const cancelTimeout = addTimeout(delays.read, timeoutHandler, 'read');
|
|
once(response, 'end', cancelTimeout);
|
|
});
|
|
}
|
|
return cancelTimeouts;
|
|
}
|