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; }