• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2require('./patch-core');
3const inherits = require('util').inherits;
4const promisify = require('es6-promisify');
5const EventEmitter = require('events').EventEmitter;
6
7module.exports = Agent;
8
9function isAgent(v) {
10  return v && typeof v.addRequest === 'function';
11}
12
13/**
14 * Base `http.Agent` implementation.
15 * No pooling/keep-alive is implemented by default.
16 *
17 * @param {Function} callback
18 * @api public
19 */
20function Agent(callback, _opts) {
21  if (!(this instanceof Agent)) {
22    return new Agent(callback, _opts);
23  }
24
25  EventEmitter.call(this);
26
27  // The callback gets promisified if it has 3 parameters
28  // (i.e. it has a callback function) lazily
29  this._promisifiedCallback = false;
30
31  let opts = _opts;
32  if ('function' === typeof callback) {
33    this.callback = callback;
34  } else if (callback) {
35    opts = callback;
36  }
37
38  // timeout for the socket to be returned from the callback
39  this.timeout = (opts && opts.timeout) || null;
40
41  this.options = opts;
42}
43inherits(Agent, EventEmitter);
44
45/**
46 * Override this function in your subclass!
47 */
48Agent.prototype.callback = function callback(req, opts) {
49  throw new Error(
50    '"agent-base" has no default implementation, you must subclass and override `callback()`'
51  );
52};
53
54/**
55 * Called by node-core's "_http_client.js" module when creating
56 * a new HTTP request with this Agent instance.
57 *
58 * @api public
59 */
60Agent.prototype.addRequest = function addRequest(req, _opts) {
61  const ownOpts = Object.assign({}, _opts);
62
63  // Set default `host` for HTTP to localhost
64  if (null == ownOpts.host) {
65    ownOpts.host = 'localhost';
66  }
67
68  // Set default `port` for HTTP if none was explicitly specified
69  if (null == ownOpts.port) {
70    ownOpts.port = ownOpts.secureEndpoint ? 443 : 80;
71  }
72
73  const opts = Object.assign({}, this.options, ownOpts);
74
75  if (opts.host && opts.path) {
76    // If both a `host` and `path` are specified then it's most likely the
77    // result of a `url.parse()` call... we need to remove the `path` portion so
78    // that `net.connect()` doesn't attempt to open that as a unix socket file.
79    delete opts.path;
80  }
81
82  delete opts.agent;
83  delete opts.hostname;
84  delete opts._defaultAgent;
85  delete opts.defaultPort;
86  delete opts.createConnection;
87
88  // Hint to use "Connection: close"
89  // XXX: non-documented `http` module API :(
90  req._last = true;
91  req.shouldKeepAlive = false;
92
93  // Create the `stream.Duplex` instance
94  let timeout;
95  let timedOut = false;
96  const timeoutMs = this.timeout;
97  const freeSocket = this.freeSocket;
98
99  function onerror(err) {
100    if (req._hadError) return;
101    req.emit('error', err);
102    // For Safety. Some additional errors might fire later on
103    // and we need to make sure we don't double-fire the error event.
104    req._hadError = true;
105  }
106
107  function ontimeout() {
108    timeout = null;
109    timedOut = true;
110    const err = new Error(
111      'A "socket" was not created for HTTP request before ' + timeoutMs + 'ms'
112    );
113    err.code = 'ETIMEOUT';
114    onerror(err);
115  }
116
117  function callbackError(err) {
118    if (timedOut) return;
119    if (timeout != null) {
120      clearTimeout(timeout);
121      timeout = null;
122    }
123    onerror(err);
124  }
125
126  function onsocket(socket) {
127    if (timedOut) return;
128    if (timeout != null) {
129      clearTimeout(timeout);
130      timeout = null;
131    }
132    if (isAgent(socket)) {
133      // `socket` is actually an http.Agent instance, so relinquish
134      // responsibility for this `req` to the Agent from here on
135      socket.addRequest(req, opts);
136    } else if (socket) {
137      function onfree() {
138        freeSocket(socket, opts);
139      }
140      socket.on('free', onfree);
141      req.onSocket(socket);
142    } else {
143      const err = new Error(
144        'no Duplex stream was returned to agent-base for `' + req.method + ' ' + req.path + '`'
145      );
146      onerror(err);
147    }
148  }
149
150  if (!this._promisifiedCallback && this.callback.length >= 3) {
151    // Legacy callback function - convert to a Promise
152    this.callback = promisify(this.callback, this);
153    this._promisifiedCallback = true;
154  }
155
156  if (timeoutMs > 0) {
157    timeout = setTimeout(ontimeout, timeoutMs);
158  }
159
160  try {
161    Promise.resolve(this.callback(req, opts)).then(onsocket, callbackError);
162  } catch (err) {
163    Promise.reject(err).catch(callbackError);
164  }
165};
166
167Agent.prototype.freeSocket = function freeSocket(socket, opts) {
168  // TODO reuse sockets
169  socket.destroy();
170};
171