UNPKG

9.02 kBPlain TextView Raw
1import net from 'net';
2import http from 'http';
3import https from 'https';
4import { Duplex } from 'stream';
5import { EventEmitter } from 'events';
6import createDebug from 'debug';
7import promisify from './promisify';
8
9const debug = createDebug('agent-base');
10
11function isAgent(v: any): v is createAgent.AgentLike {
12 return Boolean(v) && typeof v.addRequest === 'function';
13}
14
15function isSecureEndpoint(): boolean {
16 const { stack } = new Error();
17 if (typeof stack !== 'string') return false;
18 return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
19}
20
21function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
22function createAgent(
23 callback: createAgent.AgentCallback,
24 opts?: createAgent.AgentOptions
25): createAgent.Agent;
26function createAgent(
27 callback?: createAgent.AgentCallback | createAgent.AgentOptions,
28 opts?: createAgent.AgentOptions
29) {
30 return new createAgent.Agent(callback, opts);
31}
32
33namespace createAgent {
34 export interface ClientRequest extends http.ClientRequest {
35 _last?: boolean;
36 _hadError?: boolean;
37 method: string;
38 }
39
40 export interface AgentRequestOptions {
41 host?: string;
42 path?: string;
43 // `port` on `http.RequestOptions` can be a string or undefined,
44 // but `net.TcpNetConnectOpts` expects only a number
45 port: number;
46 }
47
48 export interface HttpRequestOptions
49 extends AgentRequestOptions,
50 Omit<http.RequestOptions, keyof AgentRequestOptions> {
51 secureEndpoint: false;
52 }
53
54 export interface HttpsRequestOptions
55 extends AgentRequestOptions,
56 Omit<https.RequestOptions, keyof AgentRequestOptions> {
57 secureEndpoint: true;
58 }
59
60 export type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
61
62 export type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
63
64 export type AgentCallbackReturn = Duplex | AgentLike;
65
66 export type AgentCallbackCallback = (
67 err?: Error | null,
68 socket?: createAgent.AgentCallbackReturn
69 ) => void;
70
71 export type AgentCallbackPromise = (
72 req: createAgent.ClientRequest,
73 opts: createAgent.RequestOptions
74 ) =>
75 | createAgent.AgentCallbackReturn
76 | Promise<createAgent.AgentCallbackReturn>;
77
78 export type AgentCallback = typeof Agent.prototype.callback;
79
80 export type AgentOptions = {
81 timeout?: number;
82 };
83
84 /**
85 * Base `http.Agent` implementation.
86 * No pooling/keep-alive is implemented by default.
87 *
88 * @param {Function} callback
89 * @api public
90 */
91 export class Agent extends EventEmitter {
92 public timeout: number | null;
93 public maxFreeSockets: number;
94 public maxTotalSockets: number;
95 public maxSockets: number;
96 public sockets: {
97 [key: string]: net.Socket[];
98 };
99 public freeSockets: {
100 [key: string]: net.Socket[];
101 };
102 public requests: {
103 [key: string]: http.IncomingMessage[];
104 };
105 public options: https.AgentOptions;
106 private promisifiedCallback?: createAgent.AgentCallbackPromise;
107 private explicitDefaultPort?: number;
108 private explicitProtocol?: string;
109
110 constructor(
111 callback?: createAgent.AgentCallback | createAgent.AgentOptions,
112 _opts?: createAgent.AgentOptions
113 ) {
114 super();
115
116 let opts = _opts;
117 if (typeof callback === 'function') {
118 this.callback = callback;
119 } else if (callback) {
120 opts = callback;
121 }
122
123 // Timeout for the socket to be returned from the callback
124 this.timeout = null;
125 if (opts && typeof opts.timeout === 'number') {
126 this.timeout = opts.timeout;
127 }
128
129 // These aren't actually used by `agent-base`, but are required
130 // for the TypeScript definition files in `@types/node` :/
131 this.maxFreeSockets = 1;
132 this.maxSockets = 1;
133 this.maxTotalSockets = Infinity;
134 this.sockets = {};
135 this.freeSockets = {};
136 this.requests = {};
137 this.options = {};
138 }
139
140 get defaultPort(): number {
141 if (typeof this.explicitDefaultPort === 'number') {
142 return this.explicitDefaultPort;
143 }
144 return isSecureEndpoint() ? 443 : 80;
145 }
146
147 set defaultPort(v: number) {
148 this.explicitDefaultPort = v;
149 }
150
151 get protocol(): string {
152 if (typeof this.explicitProtocol === 'string') {
153 return this.explicitProtocol;
154 }
155 return isSecureEndpoint() ? 'https:' : 'http:';
156 }
157
158 set protocol(v: string) {
159 this.explicitProtocol = v;
160 }
161
162 callback(
163 req: createAgent.ClientRequest,
164 opts: createAgent.RequestOptions,
165 fn: createAgent.AgentCallbackCallback
166 ): void;
167 callback(
168 req: createAgent.ClientRequest,
169 opts: createAgent.RequestOptions
170 ):
171 | createAgent.AgentCallbackReturn
172 | Promise<createAgent.AgentCallbackReturn>;
173 callback(
174 req: createAgent.ClientRequest,
175 opts: createAgent.AgentOptions,
176 fn?: createAgent.AgentCallbackCallback
177 ):
178 | createAgent.AgentCallbackReturn
179 | Promise<createAgent.AgentCallbackReturn>
180 | void {
181 throw new Error(
182 '"agent-base" has no default implementation, you must subclass and override `callback()`'
183 );
184 }
185
186 /**
187 * Called by node-core's "_http_client.js" module when creating
188 * a new HTTP request with this Agent instance.
189 *
190 * @api public
191 */
192 addRequest(req: ClientRequest, _opts: RequestOptions): void {
193 const opts: RequestOptions = { ..._opts };
194
195 if (typeof opts.secureEndpoint !== 'boolean') {
196 opts.secureEndpoint = isSecureEndpoint();
197 }
198
199 if (opts.host == null) {
200 opts.host = 'localhost';
201 }
202
203 if (opts.port == null) {
204 opts.port = opts.secureEndpoint ? 443 : 80;
205 }
206
207 if (opts.protocol == null) {
208 opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
209 }
210
211 if (opts.host && opts.path) {
212 // If both a `host` and `path` are specified then it's most
213 // likely the result of a `url.parse()` call... we need to
214 // remove the `path` portion so that `net.connect()` doesn't
215 // attempt to open that as a unix socket file.
216 delete opts.path;
217 }
218
219 delete opts.agent;
220 delete opts.hostname;
221 delete opts._defaultAgent;
222 delete opts.defaultPort;
223 delete opts.createConnection;
224
225 // Hint to use "Connection: close"
226 // XXX: non-documented `http` module API :(
227 req._last = true;
228 req.shouldKeepAlive = false;
229
230 let timedOut = false;
231 let timeoutId: ReturnType<typeof setTimeout> | null = null;
232 const timeoutMs = opts.timeout || this.timeout;
233
234 const onerror = (err: NodeJS.ErrnoException) => {
235 if (req._hadError) return;
236 req.emit('error', err);
237 // For Safety. Some additional errors might fire later on
238 // and we need to make sure we don't double-fire the error event.
239 req._hadError = true;
240 };
241
242 const ontimeout = () => {
243 timeoutId = null;
244 timedOut = true;
245 const err: NodeJS.ErrnoException = new Error(
246 `A "socket" was not created for HTTP request before ${timeoutMs}ms`
247 );
248 err.code = 'ETIMEOUT';
249 onerror(err);
250 };
251
252 const callbackError = (err: NodeJS.ErrnoException) => {
253 if (timedOut) return;
254 if (timeoutId !== null) {
255 clearTimeout(timeoutId);
256 timeoutId = null;
257 }
258 onerror(err);
259 };
260
261 const onsocket = (socket: AgentCallbackReturn) => {
262 if (timedOut) return;
263 if (timeoutId != null) {
264 clearTimeout(timeoutId);
265 timeoutId = null;
266 }
267
268 if (isAgent(socket)) {
269 // `socket` is actually an `http.Agent` instance, so
270 // relinquish responsibility for this `req` to the Agent
271 // from here on
272 debug(
273 'Callback returned another Agent instance %o',
274 socket.constructor.name
275 );
276 (socket as createAgent.Agent).addRequest(req, opts);
277 return;
278 }
279
280 if (socket) {
281 socket.once('free', () => {
282 this.freeSocket(socket as net.Socket, opts);
283 });
284 req.onSocket(socket as net.Socket);
285 return;
286 }
287
288 const err = new Error(
289 `no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``
290 );
291 onerror(err);
292 };
293
294 if (typeof this.callback !== 'function') {
295 onerror(new Error('`callback` is not defined'));
296 return;
297 }
298
299 if (!this.promisifiedCallback) {
300 if (this.callback.length >= 3) {
301 debug('Converting legacy callback function to promise');
302 this.promisifiedCallback = promisify(this.callback);
303 } else {
304 this.promisifiedCallback = this.callback;
305 }
306 }
307
308 if (typeof timeoutMs === 'number' && timeoutMs > 0) {
309 timeoutId = setTimeout(ontimeout, timeoutMs);
310 }
311
312 if ('port' in opts && typeof opts.port !== 'number') {
313 opts.port = Number(opts.port);
314 }
315
316 try {
317 debug(
318 'Resolving socket for %o request: %o',
319 opts.protocol,
320 `${req.method} ${req.path}`
321 );
322 Promise.resolve(this.promisifiedCallback(req, opts)).then(
323 onsocket,
324 callbackError
325 );
326 } catch (err) {
327 Promise.reject(err).catch(callbackError);
328 }
329 }
330
331 freeSocket(socket: net.Socket, opts: AgentOptions) {
332 debug('Freeing socket %o %o', socket.constructor.name, opts);
333 socket.destroy();
334 }
335
336 destroy() {
337 debug('Destroying agent %o', this.constructor.name);
338 }
339 }
340
341 // So that `instanceof` works correctly
342 createAgent.prototype = createAgent.Agent.prototype;
343}
344
345export = createAgent;