node/lib/_http_client.js

587 строки
16 KiB
JavaScript

'use strict';
const util = require('util');
const net = require('net');
const url = require('url');
const HTTPParser = process.binding('http_parser').HTTPParser;
const assert = require('assert').ok;
const common = require('_http_common');
const httpSocketSetup = common.httpSocketSetup;
const parsers = common.parsers;
const freeParser = common.freeParser;
const debug = common.debug;
const OutgoingMessage = require('_http_outgoing').OutgoingMessage;
const Agent = require('_http_agent');
const Buffer = require('buffer').Buffer;
function ClientRequest(options, cb) {
var self = this;
OutgoingMessage.call(self);
if (typeof options === 'string') {
options = url.parse(options);
if (!options.hostname) {
throw new Error('Unable to determine the domain name');
}
} else {
options = util._extend({}, options);
}
var agent = options.agent;
var defaultAgent = options._defaultAgent || Agent.globalAgent;
if (agent === false) {
agent = new defaultAgent.constructor();
} else if ((agent === null || agent === undefined) &&
!options.createConnection) {
agent = defaultAgent;
}
self.agent = agent;
var protocol = options.protocol || defaultAgent.protocol;
var expectedProtocol = defaultAgent.protocol;
if (self.agent && self.agent.protocol)
expectedProtocol = self.agent.protocol;
if (options.path && / /.test(options.path)) {
// The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/
// with an additional rule for ignoring percentage-escaped characters
// but that's a) hard to capture in a regular expression that performs
// well, and b) possibly too restrictive for real-world usage. That's
// why it only scans for spaces because those are guaranteed to create
// an invalid request.
throw new TypeError('Request path contains unescaped characters');
} else if (protocol !== expectedProtocol) {
throw new Error('Protocol "' + protocol + '" not supported. ' +
'Expected "' + expectedProtocol + '"');
}
const defaultPort = options.defaultPort ||
self.agent && self.agent.defaultPort;
var port = options.port = options.port || defaultPort || 80;
var host = options.host = options.hostname || options.host || 'localhost';
if (options.setHost === undefined) {
var setHost = true;
}
self.socketPath = options.socketPath;
var method = self.method = (options.method || 'GET').toUpperCase();
if (!common._checkIsHttpToken(method)) {
throw new TypeError('Method must be a valid HTTP token');
}
self.path = options.path || '/';
if (cb) {
self.once('response', cb);
}
if (!Array.isArray(options.headers)) {
if (options.headers) {
var keys = Object.keys(options.headers);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
self.setHeader(key, options.headers[key]);
}
}
if (host && !this.getHeader('host') && setHost) {
var hostHeader = host;
if (port && +port !== defaultPort) {
hostHeader += ':' + port;
}
this.setHeader('Host', hostHeader);
}
}
if (options.auth && !this.getHeader('Authorization')) {
//basic auth
this.setHeader('Authorization', 'Basic ' +
new Buffer(options.auth).toString('base64'));
}
if (method === 'GET' ||
method === 'HEAD' ||
method === 'DELETE' ||
method === 'OPTIONS' ||
method === 'CONNECT') {
self.useChunkedEncodingByDefault = false;
} else {
self.useChunkedEncodingByDefault = true;
}
if (Array.isArray(options.headers)) {
self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n',
options.headers);
} else if (self.getHeader('expect')) {
self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n',
self._renderHeaders());
}
if (self.socketPath) {
self._last = true;
self.shouldKeepAlive = false;
self.onSocket(self.agent.createConnection({ path: self.socketPath }));
} else if (self.agent) {
// If there is an agent we should default to Connection:keep-alive,
// but only if the Agent will actually reuse the connection!
// If it's not a keepAlive agent, and the maxSockets==Infinity, then
// there's never a case where this socket will actually be reused
if (!self.agent.keepAlive && !Number.isFinite(self.agent.maxSockets)) {
self._last = true;
self.shouldKeepAlive = false;
} else {
self._last = false;
self.shouldKeepAlive = true;
}
self.agent.addRequest(self, options);
} else {
// No agent, default to Connection:close.
self._last = true;
self.shouldKeepAlive = false;
if (options.createConnection) {
self.onSocket(options.createConnection(options));
} else {
debug('CLIENT use net.createConnection', options);
self.onSocket(net.createConnection(options));
}
}
self._deferToConnect(null, null, function() {
self._flush();
self = null;
});
}
util.inherits(ClientRequest, OutgoingMessage);
exports.ClientRequest = ClientRequest;
ClientRequest.prototype.aborted = undefined;
ClientRequest.prototype._finish = function() {
DTRACE_HTTP_CLIENT_REQUEST(this, this.connection);
LTTNG_HTTP_CLIENT_REQUEST(this, this.connection);
COUNTER_HTTP_CLIENT_REQUEST();
OutgoingMessage.prototype._finish.call(this);
};
ClientRequest.prototype._implicitHeader = function() {
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
this._renderHeaders());
};
ClientRequest.prototype.abort = function() {
if (this.aborted === undefined) {
process.nextTick(emitAbortNT, this);
}
// Mark as aborting so we can avoid sending queued request data
// This is used as a truthy flag elsewhere. The use of Date.now is for
// debugging purposes only.
this.aborted = Date.now();
// If we're aborting, we don't care about any more response data.
if (this.res)
this.res._dump();
else
this.once('response', function(res) {
res._dump();
});
// In the event that we don't have a socket, we will pop out of
// the request queue through handling in onSocket.
if (this.socket) {
// in-progress
this.socket.destroy();
}
};
function emitAbortNT(self) {
self.emit('abort');
}
function createHangUpError() {
var error = new Error('socket hang up');
error.code = 'ECONNRESET';
return error;
}
function socketCloseListener() {
var socket = this;
var req = socket._httpMessage;
debug('HTTP socket close');
// Pull through final chunk, if anything is buffered.
// the ondata function will handle it properly, and this
// is a no-op if no final chunk remains.
socket.read();
// NOTE: It's important to get parser here, because it could be freed by
// the `socketOnData`.
var parser = socket.parser;
req.emit('close');
if (req.res && req.res.readable) {
// Socket closed before we emitted 'end' below.
req.res.emit('aborted');
var res = req.res;
res.on('end', function() {
res.emit('close');
});
res.push(null);
} else if (!req.res && !req.socket._hadError) {
// This socket error fired before we started to
// receive a response. The error needs to
// fire on the request.
req.emit('error', createHangUpError());
req.socket._hadError = true;
}
// Too bad. That output wasn't getting written.
// This is pretty terrible that it doesn't raise an error.
// Fixed better in v0.10
if (req.output)
req.output.length = 0;
if (req.outputEncodings)
req.outputEncodings.length = 0;
if (parser) {
parser.finish();
freeParser(parser, req, socket);
}
}
function socketErrorListener(err) {
var socket = this;
var req = socket._httpMessage;
debug('SOCKET ERROR:', err.message, err.stack);
if (req) {
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.socket._hadError = true;
}
// Handle any pending data
socket.read();
var parser = socket.parser;
if (parser) {
parser.finish();
freeParser(parser, req, socket);
}
// Ensure that no further data will come out of the socket
socket.removeListener('data', socketOnData);
socket.removeListener('end', socketOnEnd);
socket.destroy();
}
function freeSocketErrorListener(err) {
var socket = this;
debug('SOCKET ERROR on FREE socket:', err.message, err.stack);
socket.destroy();
socket.emit('agentRemove');
}
function socketOnEnd() {
var socket = this;
var req = this._httpMessage;
var parser = this.parser;
if (!req.res && !req.socket._hadError) {
// If we don't have a response then we know that the socket
// ended prematurely and we need to emit an error on the request.
req.emit('error', createHangUpError());
req.socket._hadError = true;
}
if (parser) {
parser.finish();
freeParser(parser, req, socket);
}
socket.destroy();
}
function socketOnData(d) {
var socket = this;
var req = this._httpMessage;
var parser = this.parser;
assert(parser && parser.socket === socket);
var ret = parser.execute(d);
if (ret instanceof Error) {
debug('parse error');
freeParser(parser, req, socket);
socket.destroy();
req.emit('error', ret);
req.socket._hadError = true;
} else if (parser.incoming && parser.incoming.upgrade) {
// Upgrade or CONNECT
var bytesParsed = ret;
var res = parser.incoming;
req.res = res;
socket.removeListener('data', socketOnData);
socket.removeListener('end', socketOnEnd);
parser.finish();
var bodyHead = d.slice(bytesParsed, d.length);
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
if (req.listenerCount(eventName) > 0) {
req.upgradeOrConnect = true;
// detach the socket
socket.emit('agentRemove');
socket.removeListener('close', socketCloseListener);
socket.removeListener('error', socketErrorListener);
// TODO(isaacs): Need a way to reset a stream to fresh state
// IE, not flowing, and not explicitly paused.
socket._readableState.flowing = null;
req.emit(eventName, res, socket, bodyHead);
req.emit('close');
} else {
// Got Upgrade header or CONNECT method, but have no handler.
socket.destroy();
}
freeParser(parser, req, socket);
} else if (parser.incoming && parser.incoming.complete &&
// When the status code is 100 (Continue), the server will
// send a final response after this client sends a request
// body. So, we must not free the parser.
parser.incoming.statusCode !== 100) {
socket.removeListener('data', socketOnData);
socket.removeListener('end', socketOnEnd);
freeParser(parser, req, socket);
}
}
// client
function parserOnIncomingClient(res, shouldKeepAlive) {
var socket = this.socket;
var req = socket._httpMessage;
// propagate "domain" setting...
if (req.domain && !res.domain) {
debug('setting "res.domain"');
res.domain = req.domain;
}
debug('AGENT incoming response!');
if (req.res) {
// We already have a response object, this means the server
// sent a double response.
socket.destroy();
return;
}
req.res = res;
// Responses to CONNECT request is handled as Upgrade.
if (req.method === 'CONNECT') {
res.upgrade = true;
return true; // skip body
}
// Responses to HEAD requests are crazy.
// HEAD responses aren't allowed to have an entity-body
// but *can* have a content-length which actually corresponds
// to the content-length of the entity-body had the request
// been a GET.
var isHeadResponse = req.method === 'HEAD';
debug('AGENT isHeadResponse', isHeadResponse);
if (res.statusCode === 100) {
// restart the parser, as this is a continue message.
delete req.res; // Clear res so that we don't hit double-responses.
req.emit('continue');
return true;
}
if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) {
// Server MUST respond with Connection:keep-alive for us to enable it.
// If we've been upgraded (via WebSockets) we also shouldn't try to
// keep the connection open.
req.shouldKeepAlive = false;
}
DTRACE_HTTP_CLIENT_RESPONSE(socket, req);
LTTNG_HTTP_CLIENT_RESPONSE(socket, req);
COUNTER_HTTP_CLIENT_RESPONSE();
req.res = res;
res.req = req;
// add our listener first, so that we guarantee socket cleanup
res.on('end', responseOnEnd);
var handled = req.emit('response', res);
// If the user did not listen for the 'response' event, then they
// can't possibly read the data, so we ._dump() it into the void
// so that the socket doesn't hang there in a paused state.
if (!handled)
res._dump();
return isHeadResponse;
}
// client
function responseOnEnd() {
var res = this;
var req = res.req;
var socket = req.socket;
if (!req.shouldKeepAlive) {
if (socket.writable) {
debug('AGENT socket.destroySoon()');
socket.destroySoon();
}
assert(!socket.writable);
} else {
debug('AGENT socket keep-alive');
if (req.timeoutCb) {
socket.setTimeout(0, req.timeoutCb);
req.timeoutCb = null;
}
socket.removeListener('close', socketCloseListener);
socket.removeListener('error', socketErrorListener);
socket.once('error', freeSocketErrorListener);
// Mark this socket as available, AFTER user-added end
// handlers have a chance to run.
process.nextTick(emitFreeNT, socket);
}
}
function emitFreeNT(socket) {
socket.emit('free');
}
function tickOnSocket(req, socket) {
var parser = parsers.alloc();
req.socket = socket;
req.connection = socket;
parser.reinitialize(HTTPParser.RESPONSE);
parser.socket = socket;
parser.incoming = null;
parser.outgoing = req;
req.parser = parser;
socket.parser = parser;
socket._httpMessage = req;
// Setup "drain" propagation.
httpSocketSetup(socket);
// Propagate headers limit from request object to parser
if (typeof req.maxHeadersCount === 'number') {
parser.maxHeaderPairs = req.maxHeadersCount << 1;
} else {
// Set default value because parser may be reused from FreeList
parser.maxHeaderPairs = 2000;
}
parser.onIncoming = parserOnIncomingClient;
socket.removeListener('error', freeSocketErrorListener);
socket.on('error', socketErrorListener);
socket.on('data', socketOnData);
socket.on('end', socketOnEnd);
socket.on('close', socketCloseListener);
req.emit('socket', socket);
}
ClientRequest.prototype.onSocket = function(socket) {
process.nextTick(onSocketNT, this, socket);
};
function onSocketNT(req, socket) {
if (req.aborted) {
// If we were aborted while waiting for a socket, skip the whole thing.
socket.emit('free');
} else {
tickOnSocket(req, socket);
}
}
ClientRequest.prototype._deferToConnect = function(method, arguments_, cb) {
// This function is for calls that need to happen once the socket is
// connected and writable. It's an important promisy thing for all the socket
// calls that happen either now (when a socket is assigned) or
// in the future (when a socket gets assigned out of the pool and is
// eventually writable).
var self = this;
function callSocketMethod() {
if (method)
self.socket[method].apply(self.socket, arguments_);
if (typeof cb === 'function')
cb();
}
var onSocket = function() {
if (self.socket.writable) {
callSocketMethod();
} else {
self.socket.once('connect', callSocketMethod);
}
};
if (!self.socket) {
self.once('socket', onSocket);
} else {
onSocket();
}
};
ClientRequest.prototype.setTimeout = function(msecs, callback) {
if (callback) this.once('timeout', callback);
var self = this;
function emitTimeout() {
self.emit('timeout');
}
if (this.socket && this.socket.writable) {
if (this.timeoutCb)
this.socket.setTimeout(0, this.timeoutCb);
this.timeoutCb = emitTimeout;
this.socket.setTimeout(msecs, emitTimeout);
return this;
}
// Set timeoutCb so that it'll get cleaned up on request end
this.timeoutCb = emitTimeout;
if (this.socket) {
var sock = this.socket;
this.socket.once('connect', function() {
sock.setTimeout(msecs, emitTimeout);
});
return this;
}
this.once('socket', function(sock) {
sock.setTimeout(msecs, emitTimeout);
});
return this;
};
ClientRequest.prototype.setNoDelay = function() {
this._deferToConnect('setNoDelay', arguments);
};
ClientRequest.prototype.setSocketKeepAlive = function() {
this._deferToConnect('setKeepAlive', arguments);
};
ClientRequest.prototype.clearTimeout = function(cb) {
this.setTimeout(0, cb);
};