summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorTimothy J Fontaine <tjfontaine@gmail.com>2013-04-11 15:37:14 -0700
committerBen Noordhuis <info@bnoordhuis.nl>2013-04-17 00:08:28 +0200
commitdc9f97b7b99f27eaf21faf29c7df8c9d823ef863 (patch)
tree8e44a1187e9fe9731f944e538e9bceeb02ed58ed /lib
parentbb56489f21da9d668c279edb45d6a90d03517a30 (diff)
downloadandroid-node-v8-dc9f97b7b99f27eaf21faf29c7df8c9d823ef863.tar.gz
android-node-v8-dc9f97b7b99f27eaf21faf29c7df8c9d823ef863.tar.bz2
android-node-v8-dc9f97b7b99f27eaf21faf29c7df8c9d823ef863.zip
http: move OutgoingMessage into it's own file
Diffstat (limited to 'lib')
-rw-r--r--lib/_http_common.js13
-rw-r--r--lib/_http_outgoing.js646
-rw-r--r--lib/http.js620
3 files changed, 665 insertions, 614 deletions
diff --git a/lib/_http_common.js b/lib/_http_common.js
index 2c6b7d5d3a..40a4b11840 100644
--- a/lib/_http_common.js
+++ b/lib/_http_common.js
@@ -27,6 +27,19 @@ var IncomingMessage = incoming.IncomingMessage;
var readStart = incoming.readStart;
var readStop = incoming.readStop;
+
+var debug;
+if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) {
+ debug = function(x) { console.error('HTTP: %s', x); };
+} else {
+ debug = function() { };
+}
+exports.debug = debug;
+
+exports.CRLF = '\r\n';
+exports.chunkExpression = /chunk/i;
+exports.continueExpression = /100-continue/i;
+
// Only called in the slow case where slow means
// that the request headers were either fragmented
// across multiple TCP packets or too large to be
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
new file mode 100644
index 0000000000..4fe0927cc7
--- /dev/null
+++ b/lib/_http_outgoing.js
@@ -0,0 +1,646 @@
+// 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.
+
+var assert = require('assert').ok;
+var Stream = require('stream');
+var util = require('util');
+
+var common = require('_http_common');
+
+var CRLF = common.CRLF;
+var chunkExpression = common.chunkExpression;
+var continueExpression = common.continueExpression;
+var debug = common.debug;
+
+
+var connectionExpression = /Connection/i;
+var transferEncodingExpression = /Transfer-Encoding/i;
+var closeExpression = /close/i;
+var contentLengthExpression = /Content-Length/i;
+var dateExpression = /Date/i;
+var expectExpression = /Expect/i;
+
+
+var dateCache;
+function utcDate() {
+ if (!dateCache) {
+ var d = new Date();
+ dateCache = d.toUTCString();
+ setTimeout(function() {
+ dateCache = undefined;
+ }, 1000 - d.getMilliseconds());
+ }
+ return dateCache;
+}
+
+
+function OutgoingMessage() {
+ Stream.call(this);
+
+ this.output = [];
+ this.outputEncodings = [];
+
+ this.writable = true;
+
+ this._last = false;
+ this.chunkedEncoding = false;
+ this.shouldKeepAlive = true;
+ this.useChunkedEncodingByDefault = true;
+ this.sendDate = false;
+
+ this._hasBody = true;
+ this._trailer = '';
+
+ this.finished = false;
+ this._hangupClose = false;
+
+ this.socket = null;
+ this.connection = null;
+}
+util.inherits(OutgoingMessage, Stream);
+
+
+exports.OutgoingMessage = OutgoingMessage;
+
+
+OutgoingMessage.prototype.setTimeout = function(msecs, callback) {
+ if (callback)
+ this.on('timeout', callback);
+ if (!this.socket) {
+ this.once('socket', function(socket) {
+ socket.setTimeout(msecs);
+ });
+ } else
+ this.socket.setTimeout(msecs);
+};
+
+
+OutgoingMessage.prototype.destroy = function(error) {
+ this.socket.destroy(error);
+};
+
+
+// This abstract either writing directly to the socket or buffering it.
+OutgoingMessage.prototype._send = function(data, encoding) {
+ // This is a shameful hack to get the headers and first body chunk onto
+ // the same packet. Future versions of Node are going to take care of
+ // this at a lower level and in a more general way.
+ if (!this._headerSent) {
+ if (typeof data === 'string') {
+ data = this._header + data;
+ } else {
+ this.output.unshift(this._header);
+ this.outputEncodings.unshift('ascii');
+ }
+ this._headerSent = true;
+ }
+ return this._writeRaw(data, encoding);
+};
+
+
+OutgoingMessage.prototype._writeRaw = function(data, encoding) {
+ if (data.length === 0) {
+ return true;
+ }
+
+ if (this.connection &&
+ this.connection._httpMessage === this &&
+ this.connection.writable &&
+ !this.connection.destroyed) {
+ // There might be pending data in the this.output buffer.
+ while (this.output.length) {
+ if (!this.connection.writable) {
+ this._buffer(data, encoding);
+ return false;
+ }
+ var c = this.output.shift();
+ var e = this.outputEncodings.shift();
+ this.connection.write(c, e);
+ }
+
+ // Directly write to socket.
+ return this.connection.write(data, encoding);
+ } else if (this.connection && this.connection.destroyed) {
+ // The socket was destroyed. If we're still trying to write to it,
+ // then we haven't gotten the 'close' event yet.
+ return false;
+ } else {
+ // buffer, as long as we're not destroyed.
+ this._buffer(data, encoding);
+ return false;
+ }
+};
+
+
+OutgoingMessage.prototype._buffer = function(data, encoding) {
+ if (data.length === 0) return;
+
+ var length = this.output.length;
+
+ if (length === 0 || typeof data != 'string') {
+ this.output.push(data);
+ this.outputEncodings.push(encoding);
+ return false;
+ }
+
+ var lastEncoding = this.outputEncodings[length - 1];
+ var lastData = this.output[length - 1];
+
+ if ((encoding && lastEncoding === encoding) ||
+ (!encoding && data.constructor === lastData.constructor)) {
+ this.output[length - 1] = lastData + data;
+ return false;
+ }
+
+ this.output.push(data);
+ this.outputEncodings.push(encoding);
+
+ return false;
+};
+
+
+OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
+ // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
+ // in the case of response it is: 'HTTP/1.1 200 OK\r\n'
+ var state = {
+ sentConnectionHeader: false,
+ sentContentLengthHeader: false,
+ sentTransferEncodingHeader: false,
+ sentDateHeader: false,
+ sentExpect: false,
+ messageHeader: firstLine
+ };
+
+ var field, value;
+ var self = this;
+
+ if (headers) {
+ var keys = Object.keys(headers);
+ var isArray = (Array.isArray(headers));
+ var field, value;
+
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ if (isArray) {
+ field = headers[key][0];
+ value = headers[key][1];
+ } else {
+ field = key;
+ value = headers[key];
+ }
+
+ if (Array.isArray(value)) {
+ for (var j = 0; j < value.length; j++) {
+ storeHeader(this, state, field, value[j]);
+ }
+ } else {
+ storeHeader(this, state, field, value);
+ }
+ }
+ }
+
+ // Date header
+ if (this.sendDate == true && state.sentDateHeader == false) {
+ state.messageHeader += 'Date: ' + utcDate() + CRLF;
+ }
+
+ // Force the connection to close when the response is a 204 No Content or
+ // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked"
+ // header.
+ //
+ // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but
+ // node.js used to send out a zero chunk anyway to accommodate clients
+ // that don't have special handling for those responses.
+ //
+ // It was pointed out that this might confuse reverse proxies to the point
+ // of creating security liabilities, so suppress the zero chunk and force
+ // the connection to close.
+ var statusCode = this.statusCode;
+ if ((statusCode == 204 || statusCode === 304) &&
+ this.chunkedEncoding === true) {
+ debug(statusCode + ' response should not use chunked encoding,' +
+ ' closing connection.');
+ this.chunkedEncoding = false;
+ this.shouldKeepAlive = false;
+ }
+
+ // keep-alive logic
+ if (state.sentConnectionHeader === false) {
+ var shouldSendKeepAlive = this.shouldKeepAlive &&
+ (state.sentContentLengthHeader ||
+ this.useChunkedEncodingByDefault ||
+ this.agent);
+ if (shouldSendKeepAlive) {
+ state.messageHeader += 'Connection: keep-alive\r\n';
+ } else {
+ this._last = true;
+ state.messageHeader += 'Connection: close\r\n';
+ }
+ }
+
+ if (state.sentContentLengthHeader == false &&
+ state.sentTransferEncodingHeader == false) {
+ if (this._hasBody) {
+ if (this.useChunkedEncodingByDefault) {
+ state.messageHeader += 'Transfer-Encoding: chunked\r\n';
+ this.chunkedEncoding = true;
+ } else {
+ this._last = true;
+ }
+ } else {
+ // Make sure we don't end the 0\r\n\r\n at the end of the message.
+ this.chunkedEncoding = false;
+ }
+ }
+
+ this._header = state.messageHeader + CRLF;
+ this._headerSent = false;
+
+ // wait until the first body chunk, or close(), is sent to flush,
+ // UNLESS we're sending Expect: 100-continue.
+ if (state.sentExpect) this._send('');
+};
+
+function storeHeader(self, state, field, value) {
+ // Protect against response splitting. The if statement is there to
+ // minimize the performance impact in the common case.
+ if (/[\r\n]/.test(value))
+ value = value.replace(/[\r\n]+[ \t]*/g, '');
+
+ state.messageHeader += field + ': ' + value + CRLF;
+
+ if (connectionExpression.test(field)) {
+ state.sentConnectionHeader = true;
+ if (closeExpression.test(value)) {
+ self._last = true;
+ } else {
+ self.shouldKeepAlive = true;
+ }
+
+ } else if (transferEncodingExpression.test(field)) {
+ state.sentTransferEncodingHeader = true;
+ if (chunkExpression.test(value)) self.chunkedEncoding = true;
+
+ } else if (contentLengthExpression.test(field)) {
+ state.sentContentLengthHeader = true;
+ } else if (dateExpression.test(field)) {
+ state.sentDateHeader = true;
+ } else if (expectExpression.test(field)) {
+ state.sentExpect = true;
+ }
+}
+
+
+OutgoingMessage.prototype.setHeader = function(name, value) {
+ if (arguments.length < 2) {
+ throw new Error('`name` and `value` are required for setHeader().');
+ }
+
+ if (this._header) {
+ throw new Error('Can\'t set headers after they are sent.');
+ }
+
+ var key = name.toLowerCase();
+ this._headers = this._headers || {};
+ this._headerNames = this._headerNames || {};
+ this._headers[key] = value;
+ this._headerNames[key] = name;
+};
+
+
+OutgoingMessage.prototype.getHeader = function(name) {
+ if (arguments.length < 1) {
+ throw new Error('`name` is required for getHeader().');
+ }
+
+ if (!this._headers) return;
+
+ var key = name.toLowerCase();
+ return this._headers[key];
+};
+
+
+OutgoingMessage.prototype.removeHeader = function(name) {
+ if (arguments.length < 1) {
+ throw new Error('`name` is required for removeHeader().');
+ }
+
+ if (this._header) {
+ throw new Error('Can\'t remove headers after they are sent.');
+ }
+
+ if (!this._headers) return;
+
+ var key = name.toLowerCase();
+ delete this._headers[key];
+ delete this._headerNames[key];
+};
+
+
+OutgoingMessage.prototype._renderHeaders = function() {
+ if (this._header) {
+ throw new Error('Can\'t render headers after they are sent to the client.');
+ }
+
+ if (!this._headers) return {};
+
+ var headers = {};
+ var keys = Object.keys(this._headers);
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ headers[this._headerNames[key]] = this._headers[key];
+ }
+ return headers;
+};
+
+
+Object.defineProperty(OutgoingMessage.prototype, 'headersSent', {
+ configurable: true,
+ enumerable: true,
+ get: function() { return !!this._header; }
+});
+
+
+OutgoingMessage.prototype.write = function(chunk, encoding) {
+ if (!this._header) {
+ this._implicitHeader();
+ }
+
+ if (!this._hasBody) {
+ debug('This type of response MUST NOT have a body. ' +
+ 'Ignoring write() calls.');
+ return true;
+ }
+
+ if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) {
+ throw new TypeError('first argument must be a string or Buffer');
+ }
+
+ if (chunk.length === 0) return false;
+
+ var len, ret;
+ if (this.chunkedEncoding) {
+ if (typeof(chunk) === 'string' &&
+ encoding !== 'hex' &&
+ encoding !== 'base64' &&
+ encoding !== 'binary') {
+ len = Buffer.byteLength(chunk, encoding);
+ chunk = len.toString(16) + CRLF + chunk + CRLF;
+ ret = this._send(chunk, encoding);
+ } else {
+ // buffer, or a non-toString-friendly encoding
+ len = chunk.length;
+ this._send(len.toString(16) + CRLF);
+ this._send(chunk, encoding);
+ ret = this._send(CRLF);
+ }
+ } else {
+ ret = this._send(chunk, encoding);
+ }
+
+ debug('write ret = ' + ret);
+ return ret;
+};
+
+
+OutgoingMessage.prototype.addTrailers = function(headers) {
+ this._trailer = '';
+ var keys = Object.keys(headers);
+ var isArray = (Array.isArray(headers));
+ var field, value;
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ if (isArray) {
+ field = headers[key][0];
+ value = headers[key][1];
+ } else {
+ field = key;
+ value = headers[key];
+ }
+
+ this._trailer += field + ': ' + value + CRLF;
+ }
+};
+
+
+var zero_chunk_buf = new Buffer('\r\n0\r\n');
+var crlf_buf = new Buffer('\r\n');
+
+
+OutgoingMessage.prototype.end = function(data, encoding) {
+ if (this.finished) {
+ return false;
+ }
+ if (!this._header) {
+ this._implicitHeader();
+ }
+
+ if (data && !this._hasBody) {
+ debug('This type of response MUST NOT have a body. ' +
+ 'Ignoring data passed to end().');
+ data = false;
+ }
+
+ var ret;
+
+ var hot = this._headerSent === false &&
+ (data && data.length > 0) &&
+ this.output.length === 0 &&
+ this.connection &&
+ this.connection.writable &&
+ this.connection._httpMessage === this;
+
+ // The benefits of the hot-path optimization below start to fall
+ // off when the buffer size gets up near 128KB, because the cost
+ // of the copy is more than the cost of the extra write() call.
+ // Switch to the write/end method at that point. Heuristics and
+ // magic numbers are awful, but slow http responses are worse.
+ if (hot && Buffer.isBuffer(data) && data.length > 120 * 1024)
+ hot = false;
+
+ if (hot) {
+ // Hot path. They're doing
+ // res.writeHead();
+ // res.end(blah);
+ // HACKY.
+
+ if (typeof data === 'string') {
+ if (this.chunkedEncoding) {
+ var l = Buffer.byteLength(data, encoding).toString(16);
+ ret = this.connection.write(this._header + l + CRLF +
+ data + '\r\n0\r\n' +
+ this._trailer + '\r\n', encoding);
+ } else {
+ ret = this.connection.write(this._header + data, encoding);
+ }
+ } else if (Buffer.isBuffer(data)) {
+ if (this.chunkedEncoding) {
+ var chunk_size = data.length.toString(16);
+
+ // Skip expensive Buffer.byteLength() calls; only ISO-8859-1 characters
+ // are allowed in HTTP headers. Therefore:
+ //
+ // this._header.length == Buffer.byteLength(this._header.length)
+ // this._trailer.length == Buffer.byteLength(this._trailer.length)
+ //
+ var header_len = this._header.length;
+ var chunk_size_len = chunk_size.length;
+ var data_len = data.length;
+ var trailer_len = this._trailer.length;
+
+ var len = header_len +
+ chunk_size_len +
+ 2 + // '\r\n'.length
+ data_len +
+ 5 + // '\r\n0\r\n'.length
+ trailer_len +
+ 2; // '\r\n'.length
+
+ var buf = new Buffer(len);
+ var off = 0;
+
+ buf.write(this._header, off, header_len, 'ascii');
+ off += header_len;
+
+ buf.write(chunk_size, off, chunk_size_len, 'ascii');
+ off += chunk_size_len;
+
+ crlf_buf.copy(buf, off);
+ off += 2;
+
+ data.copy(buf, off);
+ off += data_len;
+
+ zero_chunk_buf.copy(buf, off);
+ off += 5;
+
+ if (trailer_len > 0) {
+ buf.write(this._trailer, off, trailer_len, 'ascii');
+ off += trailer_len;
+ }
+
+ crlf_buf.copy(buf, off);
+
+ ret = this.connection.write(buf);
+ } else {
+ var header_len = this._header.length;
+ var buf = new Buffer(header_len + data.length);
+ buf.write(this._header, 0, header_len, 'ascii');
+ data.copy(buf, header_len);
+ ret = this.connection.write(buf);
+ }
+ } else {
+ throw new TypeError('first argument must be a string or Buffer');
+ }
+ this._headerSent = true;
+
+ } else if (data) {
+ // Normal body write.
+ ret = this.write(data, encoding);
+ }
+
+ if (!hot) {
+ if (this.chunkedEncoding) {
+ ret = this._send('0\r\n' + this._trailer + '\r\n'); // Last chunk.
+ } else {
+ // Force a flush, HACK.
+ ret = this._send('');
+ }
+ }
+
+ this.finished = true;
+
+ // There is the first message on the outgoing queue, and we've sent
+ // everything to the socket.
+ debug('outgoing message end.');
+ if (this.output.length === 0 && this.connection._httpMessage === this) {
+ this._finish();
+ }
+
+ return ret;
+};
+
+
+var ServerResponse, ClientRequest;
+
+OutgoingMessage.prototype._finish = function() {
+ assert(this.connection);
+
+ if (!ServerResponse)
+ ServerResponse = require('http').ServerResponse;
+
+ if (!ClientRequest)
+ ClientRequest = require('http').ClientRequest;
+
+ if (this instanceof ServerResponse) {
+ DTRACE_HTTP_SERVER_RESPONSE(this.connection);
+ COUNTER_HTTP_SERVER_RESPONSE();
+ } else {
+ assert(this instanceof ClientRequest);
+ DTRACE_HTTP_CLIENT_REQUEST(this, this.connection);
+ COUNTER_HTTP_CLIENT_REQUEST();
+ }
+ this.emit('finish');
+};
+
+
+OutgoingMessage.prototype._flush = function() {
+ // This logic is probably a bit confusing. Let me explain a bit:
+ //
+ // In both HTTP servers and clients it is possible to queue up several
+ // outgoing messages. This is easiest to imagine in the case of a client.
+ // Take the following situation:
+ //
+ // req1 = client.request('GET', '/');
+ // req2 = client.request('POST', '/');
+ //
+ // When the user does
+ //
+ // req2.write('hello world\n');
+ //
+ // it's possible that the first request has not been completely flushed to
+ // the socket yet. Thus the outgoing messages need to be prepared to queue
+ // up data internally before sending it on further to the socket's queue.
+ //
+ // This function, outgoingFlush(), is called by both the Server and Client
+ // to attempt to flush any pending messages out to the socket.
+
+ if (!this.socket) return;
+
+ var ret;
+ while (this.output.length) {
+
+ if (!this.socket.writable) return; // XXX Necessary?
+
+ var data = this.output.shift();
+ var encoding = this.outputEncodings.shift();
+
+ ret = this.socket.write(data, encoding);
+ }
+
+ if (this.finished) {
+ // This is a queue to the server or client to bring in the next this.
+ this._finish();
+ } else if (ret) {
+ // This is necessary to prevent https from breaking
+ this.emit('drain');
+ }
+};
diff --git a/lib/http.js b/lib/http.js
index 15fff690bf..e0d3e339eb 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -21,18 +21,11 @@
var util = require('util');
var net = require('net');
-var Stream = require('stream');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var HTTPParser = process.binding('http_parser').HTTPParser;
var assert = require('assert').ok;
-var debug;
-if (process.env.NODE_DEBUG && /http/.test(process.env.NODE_DEBUG)) {
- debug = function(x) { console.error('HTTP: %s', x); };
-} else {
- debug = function() { };
-}
var incoming = require('_http_incoming');
var IncomingMessage = exports.IncomingMessage = incoming.IncomingMessage;
@@ -41,9 +34,12 @@ var IncomingMessage = exports.IncomingMessage = incoming.IncomingMessage;
var common = require('_http_common');
var parsers = exports.parsers = common.parsers;
var freeParser = common.freeParser;
+var debug = common.debug;
+var CRLF = common.CRLF;
+var continueExpression = common.continueExpression;
+var chunkExpression = common.chunkExpression;
-var CRLF = '\r\n';
var STATUS_CODES = exports.STATUS_CODES = {
100 : 'Continue',
101 : 'Switching Protocols',
@@ -104,612 +100,8 @@ var STATUS_CODES = exports.STATUS_CODES = {
};
-var connectionExpression = /Connection/i;
-var transferEncodingExpression = /Transfer-Encoding/i;
-var closeExpression = /close/i;
-var chunkExpression = /chunk/i;
-var contentLengthExpression = /Content-Length/i;
-var dateExpression = /Date/i;
-var expectExpression = /Expect/i;
-var continueExpression = /100-continue/i;
-
-var dateCache;
-function utcDate() {
- if (!dateCache) {
- var d = new Date();
- dateCache = d.toUTCString();
- setTimeout(function() {
- dateCache = undefined;
- }, 1000 - d.getMilliseconds());
- }
- return dateCache;
-}
-
-
-function OutgoingMessage() {
- Stream.call(this);
-
- this.output = [];
- this.outputEncodings = [];
-
- this.writable = true;
-
- this._last = false;
- this.chunkedEncoding = false;
- this.shouldKeepAlive = true;
- this.useChunkedEncodingByDefault = true;
- this.sendDate = false;
-
- this._hasBody = true;
- this._trailer = '';
-
- this.finished = false;
- this._hangupClose = false;
-
- this.socket = null;
- this.connection = null;
-}
-util.inherits(OutgoingMessage, Stream);
-
-
-exports.OutgoingMessage = OutgoingMessage;
-
-
-OutgoingMessage.prototype.setTimeout = function(msecs, callback) {
- if (callback)
- this.on('timeout', callback);
- if (!this.socket) {
- this.once('socket', function(socket) {
- socket.setTimeout(msecs);
- });
- } else
- this.socket.setTimeout(msecs);
-};
-
-
-OutgoingMessage.prototype.destroy = function(error) {
- this.socket.destroy(error);
-};
-
-
-// This abstract either writing directly to the socket or buffering it.
-OutgoingMessage.prototype._send = function(data, encoding) {
- // This is a shameful hack to get the headers and first body chunk onto
- // the same packet. Future versions of Node are going to take care of
- // this at a lower level and in a more general way.
- if (!this._headerSent) {
- if (typeof data === 'string') {
- data = this._header + data;
- } else {
- this.output.unshift(this._header);
- this.outputEncodings.unshift('ascii');
- }
- this._headerSent = true;
- }
- return this._writeRaw(data, encoding);
-};
-
-
-OutgoingMessage.prototype._writeRaw = function(data, encoding) {
- if (data.length === 0) {
- return true;
- }
-
- if (this.connection &&
- this.connection._httpMessage === this &&
- this.connection.writable &&
- !this.connection.destroyed) {
- // There might be pending data in the this.output buffer.
- while (this.output.length) {
- if (!this.connection.writable) {
- this._buffer(data, encoding);
- return false;
- }
- var c = this.output.shift();
- var e = this.outputEncodings.shift();
- this.connection.write(c, e);
- }
-
- // Directly write to socket.
- return this.connection.write(data, encoding);
- } else if (this.connection && this.connection.destroyed) {
- // The socket was destroyed. If we're still trying to write to it,
- // then we haven't gotten the 'close' event yet.
- return false;
- } else {
- // buffer, as long as we're not destroyed.
- this._buffer(data, encoding);
- return false;
- }
-};
-
-
-OutgoingMessage.prototype._buffer = function(data, encoding) {
- if (data.length === 0) return;
-
- var length = this.output.length;
-
- if (length === 0 || typeof data != 'string') {
- this.output.push(data);
- this.outputEncodings.push(encoding);
- return false;
- }
-
- var lastEncoding = this.outputEncodings[length - 1];
- var lastData = this.output[length - 1];
-
- if ((encoding && lastEncoding === encoding) ||
- (!encoding && data.constructor === lastData.constructor)) {
- this.output[length - 1] = lastData + data;
- return false;
- }
-
- this.output.push(data);
- this.outputEncodings.push(encoding);
-
- return false;
-};
-
-
-OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
- // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n'
- // in the case of response it is: 'HTTP/1.1 200 OK\r\n'
- var state = {
- sentConnectionHeader: false,
- sentContentLengthHeader: false,
- sentTransferEncodingHeader: false,
- sentDateHeader: false,
- sentExpect: false,
- messageHeader: firstLine
- };
-
- var field, value;
- var self = this;
-
- if (headers) {
- var keys = Object.keys(headers);
- var isArray = (Array.isArray(headers));
- var field, value;
-
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i];
- if (isArray) {
- field = headers[key][0];
- value = headers[key][1];
- } else {
- field = key;
- value = headers[key];
- }
-
- if (Array.isArray(value)) {
- for (var j = 0; j < value.length; j++) {
- storeHeader(this, state, field, value[j]);
- }
- } else {
- storeHeader(this, state, field, value);
- }
- }
- }
-
- // Date header
- if (this.sendDate == true && state.sentDateHeader == false) {
- state.messageHeader += 'Date: ' + utcDate() + CRLF;
- }
-
- // Force the connection to close when the response is a 204 No Content or
- // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked"
- // header.
- //
- // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but
- // node.js used to send out a zero chunk anyway to accommodate clients
- // that don't have special handling for those responses.
- //
- // It was pointed out that this might confuse reverse proxies to the point
- // of creating security liabilities, so suppress the zero chunk and force
- // the connection to close.
- var statusCode = this.statusCode;
- if ((statusCode == 204 || statusCode === 304) &&
- this.chunkedEncoding === true) {
- debug(statusCode + ' response should not use chunked encoding,' +
- ' closing connection.');
- this.chunkedEncoding = false;
- this.shouldKeepAlive = false;
- }
-
- // keep-alive logic
- if (state.sentConnectionHeader === false) {
- var shouldSendKeepAlive = this.shouldKeepAlive &&
- (state.sentContentLengthHeader ||
- this.useChunkedEncodingByDefault ||
- this.agent);
- if (shouldSendKeepAlive) {
- state.messageHeader += 'Connection: keep-alive\r\n';
- } else {
- this._last = true;
- state.messageHeader += 'Connection: close\r\n';
- }
- }
-
- if (state.sentContentLengthHeader == false &&
- state.sentTransferEncodingHeader == false) {
- if (this._hasBody) {
- if (this.useChunkedEncodingByDefault) {
- state.messageHeader += 'Transfer-Encoding: chunked\r\n';
- this.chunkedEncoding = true;
- } else {
- this._last = true;
- }
- } else {
- // Make sure we don't end the 0\r\n\r\n at the end of the message.
- this.chunkedEncoding = false;
- }
- }
-
- this._header = state.messageHeader + CRLF;
- this._headerSent = false;
-
- // wait until the first body chunk, or close(), is sent to flush,
- // UNLESS we're sending Expect: 100-continue.
- if (state.sentExpect) this._send('');
-};
-
-function storeHeader(self, state, field, value) {
- // Protect against response splitting. The if statement is there to
- // minimize the performance impact in the common case.
- if (/[\r\n]/.test(value))
- value = value.replace(/[\r\n]+[ \t]*/g, '');
-
- state.messageHeader += field + ': ' + value + CRLF;
-
- if (connectionExpression.test(field)) {
- state.sentConnectionHeader = true;
- if (closeExpression.test(value)) {
- self._last = true;
- } else {
- self.shouldKeepAlive = true;
- }
-
- } else if (transferEncodingExpression.test(field)) {
- state.sentTransferEncodingHeader = true;
- if (chunkExpression.test(value)) self.chunkedEncoding = true;
-
- } else if (contentLengthExpression.test(field)) {
- state.sentContentLengthHeader = true;
- } else if (dateExpression.test(field)) {
- state.sentDateHeader = true;
- } else if (expectExpression.test(field)) {
- state.sentExpect = true;
- }
-}
-
-
-OutgoingMessage.prototype.setHeader = function(name, value) {
- if (arguments.length < 2) {
- throw new Error('`name` and `value` are required for setHeader().');
- }
-
- if (this._header) {
- throw new Error('Can\'t set headers after they are sent.');
- }
-
- var key = name.toLowerCase();
- this._headers = this._headers || {};
- this._headerNames = this._headerNames || {};
- this._headers[key] = value;
- this._headerNames[key] = name;
-};
-
-
-OutgoingMessage.prototype.getHeader = function(name) {
- if (arguments.length < 1) {
- throw new Error('`name` is required for getHeader().');
- }
-
- if (!this._headers) return;
-
- var key = name.toLowerCase();
- return this._headers[key];
-};
-
-
-OutgoingMessage.prototype.removeHeader = function(name) {
- if (arguments.length < 1) {
- throw new Error('`name` is required for removeHeader().');
- }
-
- if (this._header) {
- throw new Error('Can\'t remove headers after they are sent.');
- }
-
- if (!this._headers) return;
-
- var key = name.toLowerCase();
- delete this._headers[key];
- delete this._headerNames[key];
-};
-
-
-OutgoingMessage.prototype._renderHeaders = function() {
- if (this._header) {
- throw new Error('Can\'t render headers after they are sent to the client.');
- }
-
- if (!this._headers) return {};
-
- var headers = {};
- var keys = Object.keys(this._headers);
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i];
- headers[this._headerNames[key]] = this._headers[key];
- }
- return headers;
-};
-
-
-Object.defineProperty(OutgoingMessage.prototype, 'headersSent', {
- configurable: true,
- enumerable: true,
- get: function() { return !!this._header; }
-});
-
-
-OutgoingMessage.prototype.write = function(chunk, encoding) {
- if (!this._header) {
- this._implicitHeader();
- }
-
- if (!this._hasBody) {
- debug('This type of response MUST NOT have a body. ' +
- 'Ignoring write() calls.');
- return true;
- }
-
- if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk)) {
- throw new TypeError('first argument must be a string or Buffer');
- }
-
- if (chunk.length === 0) return false;
-
- var len, ret;
- if (this.chunkedEncoding) {
- if (typeof(chunk) === 'string' &&
- encoding !== 'hex' &&
- encoding !== 'base64' &&
- encoding !== 'binary') {
- len = Buffer.byteLength(chunk, encoding);
- chunk = len.toString(16) + CRLF + chunk + CRLF;
- ret = this._send(chunk, encoding);
- } else {
- // buffer, or a non-toString-friendly encoding
- len = chunk.length;
- this._send(len.toString(16) + CRLF);
- this._send(chunk, encoding);
- ret = this._send(CRLF);
- }
- } else {
- ret = this._send(chunk, encoding);
- }
-
- debug('write ret = ' + ret);
- return ret;
-};
-
-
-OutgoingMessage.prototype.addTrailers = function(headers) {
- this._trailer = '';
- var keys = Object.keys(headers);
- var isArray = (Array.isArray(headers));
- var field, value;
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i];
- if (isArray) {
- field = headers[key][0];
- value = headers[key][1];
- } else {
- field = key;
- value = headers[key];
- }
-
- this._trailer += field + ': ' + value + CRLF;
- }
-};
-
-
-var zero_chunk_buf = new Buffer('\r\n0\r\n');
-var crlf_buf = new Buffer('\r\n');
-
-
-OutgoingMessage.prototype.end = function(data, encoding) {
- if (this.finished) {
- return false;
- }
- if (!this._header) {
- this._implicitHeader();
- }
-
- if (data && !this._hasBody) {
- debug('This type of response MUST NOT have a body. ' +
- 'Ignoring data passed to end().');
- data = false;
- }
-
- var ret;
-
- var hot = this._headerSent === false &&
- (data && data.length > 0) &&
- this.output.length === 0 &&
- this.connection &&
- this.connection.writable &&
- this.connection._httpMessage === this;
-
- // The benefits of the hot-path optimization below start to fall
- // off when the buffer size gets up near 128KB, because the cost
- // of the copy is more than the cost of the extra write() call.
- // Switch to the write/end method at that point. Heuristics and
- // magic numbers are awful, but slow http responses are worse.
- if (hot && Buffer.isBuffer(data) && data.length > 120 * 1024)
- hot = false;
-
- if (hot) {
- // Hot path. They're doing
- // res.writeHead();
- // res.end(blah);
- // HACKY.
-
- if (typeof data === 'string') {
- if (this.chunkedEncoding) {
- var l = Buffer.byteLength(data, encoding).toString(16);
- ret = this.connection.write(this._header + l + CRLF +
- data + '\r\n0\r\n' +
- this._trailer + '\r\n', encoding);
- } else {
- ret = this.connection.write(this._header + data, encoding);
- }
- } else if (Buffer.isBuffer(data)) {
- if (this.chunkedEncoding) {
- var chunk_size = data.length.toString(16);
-
- // Skip expensive Buffer.byteLength() calls; only ISO-8859-1 characters
- // are allowed in HTTP headers. Therefore:
- //
- // this._header.length == Buffer.byteLength(this._header.length)
- // this._trailer.length == Buffer.byteLength(this._trailer.length)
- //
- var header_len = this._header.length;
- var chunk_size_len = chunk_size.length;
- var data_len = data.length;
- var trailer_len = this._trailer.length;
-
- var len = header_len +
- chunk_size_len +
- 2 + // '\r\n'.length
- data_len +
- 5 + // '\r\n0\r\n'.length
- trailer_len +
- 2; // '\r\n'.length
-
- var buf = new Buffer(len);
- var off = 0;
-
- buf.write(this._header, off, header_len, 'ascii');
- off += header_len;
-
- buf.write(chunk_size, off, chunk_size_len, 'ascii');
- off += chunk_size_len;
-
- crlf_buf.copy(buf, off);
- off += 2;
-
- data.copy(buf, off);
- off += data_len;
-
- zero_chunk_buf.copy(buf, off);
- off += 5;
-
- if (trailer_len > 0) {
- buf.write(this._trailer, off, trailer_len, 'ascii');
- off += trailer_len;
- }
-
- crlf_buf.copy(buf, off);
-
- ret = this.connection.write(buf);
- } else {
- var header_len = this._header.length;
- var buf = new Buffer(header_len + data.length);
- buf.write(this._header, 0, header_len, 'ascii');
- data.copy(buf, header_len);
- ret = this.connection.write(buf);
- }
- } else {
- throw new TypeError('first argument must be a string or Buffer');
- }
- this._headerSent = true;
-
- } else if (data) {
- // Normal body write.
- ret = this.write(data, encoding);
- }
-
- if (!hot) {
- if (this.chunkedEncoding) {
- ret = this._send('0\r\n' + this._trailer + '\r\n'); // Last chunk.
- } else {
- // Force a flush, HACK.
- ret = this._send('');
- }
- }
-
- this.finished = true;
-
- // There is the first message on the outgoing queue, and we've sent
- // everything to the socket.
- debug('outgoing message end.');
- if (this.output.length === 0 && this.connection._httpMessage === this) {
- this._finish();
- }
-
- return ret;
-};
-
-
-OutgoingMessage.prototype._finish = function() {
- assert(this.connection);
- if (this instanceof ServerResponse) {
- DTRACE_HTTP_SERVER_RESPONSE(this.connection);
- COUNTER_HTTP_SERVER_RESPONSE();
- } else {
- assert(this instanceof ClientRequest);
- DTRACE_HTTP_CLIENT_REQUEST(this, this.connection);
- COUNTER_HTTP_CLIENT_REQUEST();
- }
- this.emit('finish');
-};
-
-
-OutgoingMessage.prototype._flush = function() {
- // This logic is probably a bit confusing. Let me explain a bit:
- //
- // In both HTTP servers and clients it is possible to queue up several
- // outgoing messages. This is easiest to imagine in the case of a client.
- // Take the following situation:
- //
- // req1 = client.request('GET', '/');
- // req2 = client.request('POST', '/');
- //
- // When the user does
- //
- // req2.write('hello world\n');
- //
- // it's possible that the first request has not been completely flushed to
- // the socket yet. Thus the outgoing messages need to be prepared to queue
- // up data internally before sending it on further to the socket's queue.
- //
- // This function, outgoingFlush(), is called by both the Server and Client
- // to attempt to flush any pending messages out to the socket.
-
- if (!this.socket) return;
-
- var ret;
- while (this.output.length) {
-
- if (!this.socket.writable) return; // XXX Necessary?
-
- var data = this.output.shift();
- var encoding = this.outputEncodings.shift();
-
- ret = this.socket.write(data, encoding);
- }
-
- if (this.finished) {
- // This is a queue to the server or client to bring in the next this.
- this._finish();
- } else if (ret) {
- // This is necessary to prevent https from breaking
- this.emit('drain');
- }
-};
-
+var outgoing = require('_http_outgoing');
+var OutgoingMessage = exports.OutgoingMessage = outgoing.OutgoingMessage;