mirror of
https://github.com/nodejs/node.git
synced 2024-11-30 15:30:56 +01:00
620ba41694
req.socket._hadError should be set before emitting the error event. PR-URL: https://github.com/nodejs/node/pull/14659 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
764 lines
22 KiB
JavaScript
764 lines
22 KiB
JavaScript
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
'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;
|
|
const { urlToOptions, searchParamsSymbol } = require('internal/url');
|
|
const outHeadersKey = require('internal/http').outHeadersKey;
|
|
const nextTick = require('internal/process/next_tick').nextTick;
|
|
const errors = require('internal/errors');
|
|
|
|
// The actual list of disallowed characters in regexp form 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. So instead we restrict the
|
|
// filter to just control characters and spaces.
|
|
//
|
|
// This function is used in the case of small paths, where manual character code
|
|
// checks can greatly outperform the equivalent regexp (tested in V8 5.4).
|
|
function isInvalidPath(s) {
|
|
var i = 0;
|
|
if (s.charCodeAt(0) <= 32) return true;
|
|
if (++i >= s.length) return false;
|
|
if (s.charCodeAt(1) <= 32) return true;
|
|
if (++i >= s.length) return false;
|
|
if (s.charCodeAt(2) <= 32) return true;
|
|
if (++i >= s.length) return false;
|
|
if (s.charCodeAt(3) <= 32) return true;
|
|
if (++i >= s.length) return false;
|
|
if (s.charCodeAt(4) <= 32) return true;
|
|
if (++i >= s.length) return false;
|
|
if (s.charCodeAt(5) <= 32) return true;
|
|
++i;
|
|
for (; i < s.length; ++i)
|
|
if (s.charCodeAt(i) <= 32) return true;
|
|
return false;
|
|
}
|
|
|
|
function validateHost(host, name) {
|
|
if (host != null && typeof host !== 'string') {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', `options.${name}`,
|
|
['string', 'undefined', 'null'], host);
|
|
}
|
|
return host;
|
|
}
|
|
|
|
function ClientRequest(options, cb) {
|
|
OutgoingMessage.call(this);
|
|
|
|
if (typeof options === 'string') {
|
|
options = url.parse(options);
|
|
if (!options.hostname) {
|
|
throw new errors.Error('ERR_INVALID_DOMAIN_NAME');
|
|
}
|
|
} else if (options && options[searchParamsSymbol] &&
|
|
options[searchParamsSymbol][searchParamsSymbol]) {
|
|
// url.URL instance
|
|
options = urlToOptions(options);
|
|
} 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) {
|
|
if (typeof options.createConnection !== 'function') {
|
|
agent = defaultAgent;
|
|
}
|
|
// Explicitly pass through this statement as agent will not be used
|
|
// when createConnection is provided.
|
|
} else if (typeof agent.addRequest !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'Agent option',
|
|
['Agent-like object', 'undefined', 'false']);
|
|
}
|
|
this.agent = agent;
|
|
|
|
var protocol = options.protocol || defaultAgent.protocol;
|
|
var expectedProtocol = defaultAgent.protocol;
|
|
if (this.agent && this.agent.protocol)
|
|
expectedProtocol = this.agent.protocol;
|
|
|
|
var path;
|
|
if (options.path) {
|
|
path = '' + options.path;
|
|
var invalidPath;
|
|
if (path.length <= 39) { // Determined experimentally in V8 5.4
|
|
invalidPath = isInvalidPath(path);
|
|
} else {
|
|
invalidPath = /[\u0000-\u0020]/.test(path);
|
|
}
|
|
if (invalidPath)
|
|
throw new errors.TypeError('ERR_UNESCAPED_CHARACTERS', 'Request path');
|
|
}
|
|
|
|
if (protocol !== expectedProtocol) {
|
|
throw new errors.Error('ERR_INVALID_PROTOCOL', protocol, expectedProtocol);
|
|
}
|
|
|
|
var defaultPort = options.defaultPort ||
|
|
this.agent && this.agent.defaultPort;
|
|
|
|
var port = options.port = options.port || defaultPort || 80;
|
|
var host = options.host = validateHost(options.hostname, 'hostname') ||
|
|
validateHost(options.host, 'host') || 'localhost';
|
|
|
|
var setHost = (options.setHost === undefined);
|
|
|
|
this.socketPath = options.socketPath;
|
|
this.timeout = options.timeout;
|
|
|
|
var method = options.method;
|
|
var methodIsString = (typeof method === 'string');
|
|
if (method != null && !methodIsString) {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'method',
|
|
'string', method);
|
|
}
|
|
|
|
if (methodIsString && method) {
|
|
if (!common._checkIsHttpToken(method)) {
|
|
throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Method', method);
|
|
}
|
|
method = this.method = method.toUpperCase();
|
|
} else {
|
|
method = this.method = 'GET';
|
|
}
|
|
|
|
this.path = options.path || '/';
|
|
if (cb) {
|
|
this.once('response', cb);
|
|
}
|
|
|
|
var headersArray = Array.isArray(options.headers);
|
|
if (!headersArray) {
|
|
if (options.headers) {
|
|
var keys = Object.keys(options.headers);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
this.setHeader(key, options.headers[key]);
|
|
}
|
|
}
|
|
if (host && !this.getHeader('host') && setHost) {
|
|
var hostHeader = host;
|
|
|
|
// For the Host header, ensure that IPv6 addresses are enclosed
|
|
// in square brackets, as defined by URI formatting
|
|
// https://tools.ietf.org/html/rfc3986#section-3.2.2
|
|
var posColon = hostHeader.indexOf(':');
|
|
if (posColon !== -1 &&
|
|
hostHeader.indexOf(':', posColon + 1) !== -1 &&
|
|
hostHeader.charCodeAt(0) !== 91/*'['*/) {
|
|
hostHeader = `[${hostHeader}]`;
|
|
}
|
|
|
|
if (port && +port !== defaultPort) {
|
|
hostHeader += ':' + port;
|
|
}
|
|
this.setHeader('Host', hostHeader);
|
|
}
|
|
}
|
|
|
|
if (options.auth && !this.getHeader('Authorization')) {
|
|
this.setHeader('Authorization', 'Basic ' +
|
|
Buffer.from(options.auth).toString('base64'));
|
|
}
|
|
|
|
if (method === 'GET' ||
|
|
method === 'HEAD' ||
|
|
method === 'DELETE' ||
|
|
method === 'OPTIONS' ||
|
|
method === 'CONNECT') {
|
|
this.useChunkedEncodingByDefault = false;
|
|
} else {
|
|
this.useChunkedEncodingByDefault = true;
|
|
}
|
|
|
|
if (headersArray) {
|
|
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
|
|
options.headers);
|
|
} else if (this.getHeader('expect')) {
|
|
if (this._header) {
|
|
throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render');
|
|
}
|
|
|
|
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
|
|
this[outHeadersKey]);
|
|
}
|
|
|
|
this._ended = false;
|
|
this.res = null;
|
|
this.aborted = undefined;
|
|
this.timeoutCb = null;
|
|
this.upgradeOrConnect = false;
|
|
this.parser = null;
|
|
this.maxHeadersCount = null;
|
|
|
|
var called = false;
|
|
|
|
var oncreate = (err, socket) => {
|
|
if (called)
|
|
return;
|
|
called = true;
|
|
if (err) {
|
|
process.nextTick(() => this.emit('error', err));
|
|
return;
|
|
}
|
|
this.onSocket(socket);
|
|
this._deferToConnect(null, null, () => this._flush());
|
|
};
|
|
|
|
var newSocket;
|
|
if (this.socketPath) {
|
|
this._last = true;
|
|
this.shouldKeepAlive = false;
|
|
var optionsPath = {
|
|
path: this.socketPath,
|
|
timeout: this.timeout,
|
|
rejectUnauthorized: !!options.rejectUnauthorized
|
|
};
|
|
newSocket = this.agent.createConnection(optionsPath, oncreate);
|
|
if (newSocket && !called) {
|
|
called = true;
|
|
this.onSocket(newSocket);
|
|
} else {
|
|
return;
|
|
}
|
|
} else if (this.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 (!this.agent.keepAlive && !Number.isFinite(this.agent.maxSockets)) {
|
|
this._last = true;
|
|
this.shouldKeepAlive = false;
|
|
} else {
|
|
this._last = false;
|
|
this.shouldKeepAlive = true;
|
|
}
|
|
this.agent.addRequest(this, options);
|
|
} else {
|
|
// No agent, default to Connection:close.
|
|
this._last = true;
|
|
this.shouldKeepAlive = false;
|
|
if (typeof options.createConnection === 'function') {
|
|
newSocket = options.createConnection(options, oncreate);
|
|
if (newSocket && !called) {
|
|
called = true;
|
|
this.onSocket(newSocket);
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
debug('CLIENT use net.createConnection', options);
|
|
this.onSocket(net.createConnection(options));
|
|
}
|
|
}
|
|
|
|
this._deferToConnect(null, null, () => this._flush());
|
|
}
|
|
|
|
util.inherits(ClientRequest, OutgoingMessage);
|
|
|
|
|
|
ClientRequest.prototype._finish = function _finish() {
|
|
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 _implicitHeader() {
|
|
if (this._header) {
|
|
throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render');
|
|
}
|
|
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
|
|
this[outHeadersKey]);
|
|
};
|
|
|
|
ClientRequest.prototype.abort = function abort() {
|
|
if (!this.aborted) {
|
|
process.nextTick(emitAbortNT.bind(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() {
|
|
this.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.socket._hadError = true;
|
|
req.emit('error', createHangUpError());
|
|
}
|
|
|
|
// 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) {
|
|
// 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;
|
|
req.emit('error', err);
|
|
}
|
|
|
|
// 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.socket._hadError = true;
|
|
req.emit('error', createHangUpError());
|
|
}
|
|
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', ret);
|
|
freeParser(parser, req, socket);
|
|
socket.destroy();
|
|
req.socket._hadError = true;
|
|
req.emit('error', ret);
|
|
} 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 2; // skip body, and the rest
|
|
}
|
|
|
|
// 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.
|
|
req.res = null; // 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);
|
|
req.on('prefinish', requestOnPrefinish);
|
|
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 responseKeepAlive(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);
|
|
// There are cases where _handle === null. Avoid those. Passing null to
|
|
// nextTick() will call initTriggerId() to retrieve the id.
|
|
const asyncId = socket._handle ? socket._handle.getAsyncId() : null;
|
|
// Mark this socket as available, AFTER user-added end
|
|
// handlers have a chance to run.
|
|
nextTick(asyncId, emitFreeNT, socket);
|
|
}
|
|
}
|
|
|
|
function responseOnEnd() {
|
|
const res = this;
|
|
const req = this.req;
|
|
|
|
req._ended = true;
|
|
if (!req.shouldKeepAlive || req.finished)
|
|
responseKeepAlive(res, req);
|
|
}
|
|
|
|
function requestOnPrefinish() {
|
|
const req = this;
|
|
const res = this.res;
|
|
|
|
if (!req.shouldKeepAlive)
|
|
return;
|
|
|
|
if (req._ended)
|
|
responseKeepAlive(res, req);
|
|
}
|
|
|
|
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);
|
|
|
|
if (req.timeout) {
|
|
const emitRequestTimeout = () => req.emit('timeout');
|
|
socket.once('timeout', emitRequestTimeout);
|
|
req.once('response', (res) => {
|
|
res.once('end', () => {
|
|
socket.removeListener('timeout', emitRequestTimeout);
|
|
});
|
|
});
|
|
}
|
|
req.emit('socket', socket);
|
|
}
|
|
|
|
ClientRequest.prototype.onSocket = function onSocket(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.
|
|
if (req.socketPath || !req.agent) {
|
|
socket.destroy();
|
|
} else {
|
|
socket.emit('free');
|
|
}
|
|
} else {
|
|
tickOnSocket(req, socket);
|
|
}
|
|
}
|
|
|
|
ClientRequest.prototype._deferToConnect = _deferToConnect;
|
|
function _deferToConnect(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).
|
|
|
|
const callSocketMethod = () => {
|
|
if (method)
|
|
this.socket[method].apply(this.socket, arguments_);
|
|
|
|
if (typeof cb === 'function')
|
|
cb();
|
|
};
|
|
|
|
const onSocket = () => {
|
|
if (this.socket.writable) {
|
|
callSocketMethod();
|
|
} else {
|
|
this.socket.once('connect', callSocketMethod);
|
|
}
|
|
};
|
|
|
|
if (!this.socket) {
|
|
this.once('socket', onSocket);
|
|
} else {
|
|
onSocket();
|
|
}
|
|
}
|
|
|
|
ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) {
|
|
if (callback) this.once('timeout', callback);
|
|
|
|
const emitTimeout = () => this.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 setNoDelay(noDelay) {
|
|
this._deferToConnect('setNoDelay', [noDelay]);
|
|
};
|
|
|
|
ClientRequest.prototype.setSocketKeepAlive =
|
|
function setSocketKeepAlive(enable, initialDelay) {
|
|
this._deferToConnect('setKeepAlive', [enable, initialDelay]);
|
|
};
|
|
|
|
ClientRequest.prototype.clearTimeout = function clearTimeout(cb) {
|
|
this.setTimeout(0, cb);
|
|
};
|
|
|
|
module.exports = {
|
|
ClientRequest
|
|
};
|