171 lines
4.3 KiB
JavaScript
171 lines
4.3 KiB
JavaScript
|
'use strict';
|
||
|
require('./patch-core');
|
||
|
const inherits = require('util').inherits;
|
||
|
const promisify = require('es6-promisify');
|
||
|
const EventEmitter = require('events').EventEmitter;
|
||
|
|
||
|
module.exports = Agent;
|
||
|
|
||
|
function isAgent(v) {
|
||
|
return v && typeof v.addRequest === 'function';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Base `http.Agent` implementation.
|
||
|
* No pooling/keep-alive is implemented by default.
|
||
|
*
|
||
|
* @param {Function} callback
|
||
|
* @api public
|
||
|
*/
|
||
|
function Agent(callback, _opts) {
|
||
|
if (!(this instanceof Agent)) {
|
||
|
return new Agent(callback, _opts);
|
||
|
}
|
||
|
|
||
|
EventEmitter.call(this);
|
||
|
|
||
|
// The callback gets promisified if it has 3 parameters
|
||
|
// (i.e. it has a callback function) lazily
|
||
|
this._promisifiedCallback = false;
|
||
|
|
||
|
let opts = _opts;
|
||
|
if ('function' === typeof callback) {
|
||
|
this.callback = callback;
|
||
|
} else if (callback) {
|
||
|
opts = callback;
|
||
|
}
|
||
|
|
||
|
// timeout for the socket to be returned from the callback
|
||
|
this.timeout = (opts && opts.timeout) || null;
|
||
|
|
||
|
this.options = opts;
|
||
|
}
|
||
|
inherits(Agent, EventEmitter);
|
||
|
|
||
|
/**
|
||
|
* Override this function in your subclass!
|
||
|
*/
|
||
|
Agent.prototype.callback = function callback(req, opts) {
|
||
|
throw new Error(
|
||
|
'"agent-base" has no default implementation, you must subclass and override `callback()`'
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Called by node-core's "_http_client.js" module when creating
|
||
|
* a new HTTP request with this Agent instance.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
Agent.prototype.addRequest = function addRequest(req, _opts) {
|
||
|
const ownOpts = Object.assign({}, _opts);
|
||
|
|
||
|
// Set default `host` for HTTP to localhost
|
||
|
if (null == ownOpts.host) {
|
||
|
ownOpts.host = 'localhost';
|
||
|
}
|
||
|
|
||
|
// Set default `port` for HTTP if none was explicitly specified
|
||
|
if (null == ownOpts.port) {
|
||
|
ownOpts.port = ownOpts.secureEndpoint ? 443 : 80;
|
||
|
}
|
||
|
|
||
|
const opts = Object.assign({}, this.options, ownOpts);
|
||
|
|
||
|
if (opts.host && opts.path) {
|
||
|
// If both a `host` and `path` are specified then it's most likely the
|
||
|
// result of a `url.parse()` call... we need to remove the `path` portion so
|
||
|
// that `net.connect()` doesn't attempt to open that as a unix socket file.
|
||
|
delete opts.path;
|
||
|
}
|
||
|
|
||
|
delete opts.agent;
|
||
|
delete opts.hostname;
|
||
|
delete opts._defaultAgent;
|
||
|
delete opts.defaultPort;
|
||
|
delete opts.createConnection;
|
||
|
|
||
|
// Hint to use "Connection: close"
|
||
|
// XXX: non-documented `http` module API :(
|
||
|
req._last = true;
|
||
|
req.shouldKeepAlive = false;
|
||
|
|
||
|
// Create the `stream.Duplex` instance
|
||
|
let timeout;
|
||
|
let timedOut = false;
|
||
|
const timeoutMs = this.timeout;
|
||
|
const freeSocket = this.freeSocket;
|
||
|
|
||
|
function onerror(err) {
|
||
|
if (req._hadError) return;
|
||
|
req.emit('error', err);
|
||
|
// For Safety. Some additional errors might fire later on
|
||
|
// and we need to make sure we don't double-fire the error event.
|
||
|
req._hadError = true;
|
||
|
}
|
||
|
|
||
|
function ontimeout() {
|
||
|
timeout = null;
|
||
|
timedOut = true;
|
||
|
const err = new Error(
|
||
|
'A "socket" was not created for HTTP request before ' + timeoutMs + 'ms'
|
||
|
);
|
||
|
err.code = 'ETIMEOUT';
|
||
|
onerror(err);
|
||
|
}
|
||
|
|
||
|
function callbackError(err) {
|
||
|
if (timedOut) return;
|
||
|
if (timeout != null) {
|
||
|
clearTimeout(timeout);
|
||
|
timeout = null;
|
||
|
}
|
||
|
onerror(err);
|
||
|
}
|
||
|
|
||
|
function onsocket(socket) {
|
||
|
if (timedOut) return;
|
||
|
if (timeout != null) {
|
||
|
clearTimeout(timeout);
|
||
|
timeout = null;
|
||
|
}
|
||
|
if (isAgent(socket)) {
|
||
|
// `socket` is actually an http.Agent instance, so relinquish
|
||
|
// responsibility for this `req` to the Agent from here on
|
||
|
socket.addRequest(req, opts);
|
||
|
} else if (socket) {
|
||
|
function onfree() {
|
||
|
freeSocket(socket, opts);
|
||
|
}
|
||
|
socket.on('free', onfree);
|
||
|
req.onSocket(socket);
|
||
|
} else {
|
||
|
const err = new Error(
|
||
|
'no Duplex stream was returned to agent-base for `' + req.method + ' ' + req.path + '`'
|
||
|
);
|
||
|
onerror(err);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!this._promisifiedCallback && this.callback.length >= 3) {
|
||
|
// Legacy callback function - convert to a Promise
|
||
|
this.callback = promisify(this.callback, this);
|
||
|
this._promisifiedCallback = true;
|
||
|
}
|
||
|
|
||
|
if (timeoutMs > 0) {
|
||
|
timeout = setTimeout(ontimeout, timeoutMs);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
Promise.resolve(this.callback(req, opts)).then(onsocket, callbackError);
|
||
|
} catch (err) {
|
||
|
Promise.reject(err).catch(callbackError);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Agent.prototype.freeSocket = function freeSocket(socket, opts) {
|
||
|
// TODO reuse sockets
|
||
|
socket.destroy();
|
||
|
};
|