index.ts
1 import net from 'net'; 2 import http from 'http'; 3 import https from 'https'; 4 import { Duplex } from 'stream'; 5 import { EventEmitter } from 'events'; 6 import createDebug from 'debug'; 7 import promisify from './promisify'; 8 9 const debug = createDebug('agent-base'); 10 11 function isAgent(v: any): v is createAgent.AgentLike { 12 return Boolean(v) && typeof v.addRequest === 'function'; 13 } 14 15 function 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 21 function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent; 22 function createAgent( 23 callback: createAgent.AgentCallback, 24 opts?: createAgent.AgentOptions 25 ): createAgent.Agent; 26 function createAgent( 27 callback?: createAgent.AgentCallback | createAgent.AgentOptions, 28 opts?: createAgent.AgentOptions 29 ) { 30 return new createAgent.Agent(callback, opts); 31 } 32 33 namespace 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 345 export = createAgent;