diff --git a/benchmark/_http-benchmarkers.js b/benchmark/_http-benchmarkers.js index e096efc71ba162..235d5d07862e6c 100644 --- a/benchmark/_http-benchmarkers.js +++ b/benchmark/_http-benchmarkers.js @@ -61,7 +61,7 @@ WrkBenchmarker.prototype.create = function(options) { WrkBenchmarker.prototype.processResults = function(output) { const match = output.match(this.regexp); const result = match && +match[1]; - if (!result) { + if (!isFinite(result)) { return undefined; } else { return result; @@ -126,7 +126,7 @@ exports.run = function(options, callback) { } const result = benchmarker.processResults(stdout); - if (!result) { + if (result === undefined) { callback(new Error(`${options.benchmarker} produced strange output: ` + stdout, code)); return; diff --git a/benchmark/http/_http_simple.js b/benchmark/http/_http_simple.js index 644601864dd857..6886ccaf64d996 100644 --- a/benchmark/http/_http_simple.js +++ b/benchmark/http/_http_simple.js @@ -4,10 +4,10 @@ var http = require('http'); var port = parseInt(process.env.PORT || 8000); -var fixed = 'C'.repeat(20 * 1024), - storedBytes = {}, - storedBuffer = {}, - storedUnicode = {}; +var fixed = 'C'.repeat(20 * 1024); +var storedBytes = Object.create(null); +var storedBuffer = Object.create(null); +var storedUnicode = Object.create(null); var useDomains = process.env.NODE_USE_DOMAINS; @@ -29,11 +29,13 @@ var server = module.exports = http.createServer(function(req, res) { dom.add(res); } - var commands = req.url.split('/'); - var command = commands[1]; + // URL format: //// + var params = req.url.split('/'); + var command = params[1]; var body = ''; - var arg = commands[2]; - var n_chunks = parseInt(commands[3], 10); + var arg = params[2]; + var n_chunks = parseInt(params[3], 10); + var resHow = (params.length >= 5 ? params[4] : 'normal'); var status = 200; var n, i; @@ -45,7 +47,6 @@ var server = module.exports = http.createServer(function(req, res) { storedBytes[n] = 'C'.repeat(n); } body = storedBytes[n]; - } else if (command === 'buffer') { n = ~~arg; if (n <= 0) @@ -57,7 +58,6 @@ var server = module.exports = http.createServer(function(req, res) { } } body = storedBuffer[n]; - } else if (command === 'unicode') { n = ~~arg; if (n <= 0) @@ -66,23 +66,30 @@ var server = module.exports = http.createServer(function(req, res) { storedUnicode[n] = '\u263A'.repeat(n); } body = storedUnicode[n]; - } else if (command === 'quit') { res.connection.server.close(); body = 'quitting'; - } else if (command === 'fixed') { body = fixed; - } else if (command === 'echo') { - const headers = { - 'Content-Type': 'text/plain', - 'Transfer-Encoding': 'chunked' - }; - res.writeHead(200, headers); + switch (resHow) { + case 'setHeader': + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Transfer-Encoding', 'chunked'); + break; + case 'setHeaderWH': + res.setHeader('Content-Type', 'text/plain'); + res.writeHead(200, { 'Transfer-Encoding': 'chunked' }); + break; + default: + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Transfer-Encoding': 'chunked' + }); + } req.pipe(res); return; - } else { status = 404; body = 'not found\n'; @@ -91,11 +98,22 @@ var server = module.exports = http.createServer(function(req, res) { // example: http://localhost:port/bytes/512/4 // sends a 512 byte body in 4 chunks of 128 bytes if (n_chunks > 0) { - const headers = { - 'Content-Type': 'text/plain', - 'Transfer-Encoding': 'chunked' - }; - res.writeHead(status, headers); + switch (resHow) { + case 'setHeader': + res.statusCode = status; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Transfer-Encoding', 'chunked'); + break; + case 'setHeaderWH': + res.setHeader('Content-Type', 'text/plain'); + res.writeHead(status, { 'Transfer-Encoding': 'chunked' }); + break; + default: + res.writeHead(status, { + 'Content-Type': 'text/plain', + 'Transfer-Encoding': 'chunked' + }); + } // send body in chunks var len = body.length; var step = Math.floor(len / n_chunks) || 1; @@ -105,12 +123,22 @@ var server = module.exports = http.createServer(function(req, res) { } res.end(body.slice((n_chunks - 1) * step)); } else { - const headers = { - 'Content-Type': 'text/plain', - 'Content-Length': body.length.toString() - }; - - res.writeHead(status, headers); + switch (resHow) { + case 'setHeader': + res.statusCode = status; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Length', body.length.toString()); + break; + case 'setHeaderWH': + res.setHeader('Content-Type', 'text/plain'); + res.writeHead(status, { 'Content-Length': body.length.toString() }); + break; + default: + res.writeHead(status, { + 'Content-Type': 'text/plain', + 'Content-Length': body.length.toString() + }); + } res.end(body); } }); diff --git a/benchmark/http/simple.js b/benchmark/http/simple.js index 66113ed3758c48..39c8f29dc89a74 100644 --- a/benchmark/http/simple.js +++ b/benchmark/http/simple.js @@ -7,14 +7,16 @@ var bench = common.createBenchmark(main, { type: ['bytes', 'buffer'], length: [4, 1024, 102400], chunks: [0, 1, 4], // chunks=0 means 'no chunked encoding'. - c: [50, 500] + c: [50, 500], + res: ['normal', 'setHeader', 'setHeaderWH'] }); function main(conf) { process.env.PORT = PORT; var server = require('./_http_simple.js'); setTimeout(function() { - var path = '/' + conf.type + '/' + conf.length + '/' + conf.chunks; + var path = '/' + conf.type + '/' + conf.length + '/' + conf.chunks + '/' + + conf.res; bench.http({ path: path, diff --git a/lib/_http_client.js b/lib/_http_client.js index 18a58936ee9563..8e0da8a37c4b0b 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -131,8 +131,12 @@ function ClientRequest(options, cb) { self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n', options.headers); } else if (self.getHeader('expect')) { + if (self._header) { + throw new Error('Can\'t render headers after they are sent to the ' + + 'client'); + } self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n', - self._renderHeaders()); + self._headers); } this._ended = false; @@ -224,8 +228,11 @@ ClientRequest.prototype._finish = function _finish() { }; ClientRequest.prototype._implicitHeader = function _implicitHeader() { + if (this._header) { + throw new Error('Can\'t render headers after they are sent to the client'); + } this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - this._renderHeaders()); + this._headers); }; ClientRequest.prototype.abort = function abort() { diff --git a/lib/_http_common.js b/lib/_http_common.js index 2ce523fe6298bd..f585d97d7b6536 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -14,8 +14,8 @@ const debug = require('util').debuglog('http'); exports.debug = debug; exports.CRLF = '\r\n'; -exports.chunkExpression = /chunk/i; -exports.continueExpression = /100-continue/i; +exports.chunkExpression = /(?:^|\W)chunked(?:$|\W)/i; +exports.continueExpression = /(?:^|\W)100-continue(?:$|\W)/i; exports.methods = methods; const kOnHeaders = HTTPParser.kOnHeaders | 0; diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index d02f19424c0442..53419323f46b0d 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -119,6 +119,146 @@ function _addHeaderLines(headers, n) { } +// This function is used to help avoid the lowercasing of a field name if it +// matches a 'traditional cased' version of a field name. It then returns the +// lowercased name to both avoid calling toLowerCase() a second time and to +// indicate whether the field was a 'no duplicates' field. If a field is not a +// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception +// to this is the Set-Cookie header which is indicated by a `1` byte flag, since +// it is an 'array' field and thus is treated differently in _addHeaderLines(). +// TODO: perhaps http_parser could be returning both raw and lowercased versions +// of known header names to avoid us having to call toLowerCase() for those +// headers. +/* eslint-disable max-len */ +// 'array' header list is taken from: +// https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp +/* eslint-enable max-len */ +function matchKnownFields(field) { + var low = false; + while (true) { + switch (field) { + case 'Content-Type': + case 'content-type': + return 'content-type'; + case 'Content-Length': + case 'content-length': + return 'content-length'; + case 'User-Agent': + case 'user-agent': + return 'user-agent'; + case 'Referer': + case 'referer': + return 'referer'; + case 'Host': + case 'host': + return 'host'; + case 'Authorization': + case 'authorization': + return 'authorization'; + case 'Proxy-Authorization': + case 'proxy-authorization': + return 'proxy-authorization'; + case 'If-Modified-Since': + case 'if-modified-since': + return 'if-modified-since'; + case 'If-Unmodified-Since': + case 'if-unmodified-since': + return 'if-unmodified-since'; + case 'From': + case 'from': + return 'from'; + case 'Location': + case 'location': + return 'location'; + case 'Max-Forwards': + case 'max-forwards': + return 'max-forwards'; + case 'Retry-After': + case 'retry-after': + return 'retry-after'; + case 'ETag': + case 'etag': + return 'etag'; + case 'Last-Modified': + case 'last-modified': + return 'last-modified'; + case 'Server': + case 'server': + return 'server'; + case 'Age': + case 'age': + return 'age'; + case 'Expires': + case 'expires': + return 'expires'; + case 'Set-Cookie': + case 'set-cookie': + return '\u0001'; + // The fields below are not used in _addHeaderLine(), but they are common + // headers where we can avoid toLowerCase() if the mixed or lower case + // versions match the first time through. + case 'Transfer-Encoding': + case 'transfer-encoding': + return '\u0000transfer-encoding'; + case 'Date': + case 'date': + return '\u0000date'; + case 'Connection': + case 'connection': + return '\u0000connection'; + case 'Cache-Control': + case 'cache-control': + return '\u0000cache-control'; + case 'Vary': + case 'vary': + return '\u0000vary'; + case 'Content-Encoding': + case 'content-encoding': + return '\u0000content-encoding'; + case 'Cookie': + case 'cookie': + return '\u0000cookie'; + case 'Origin': + case 'origin': + return '\u0000origin'; + case 'Upgrade': + case 'upgrade': + return '\u0000upgrade'; + case 'Expect': + case 'expect': + return '\u0000expect'; + case 'If-Match': + case 'if-match': + return '\u0000if-match'; + case 'If-None-Match': + case 'if-none-match': + return '\u0000if-none-match'; + case 'Accept': + case 'accept': + return '\u0000accept'; + case 'Accept-Encoding': + case 'accept-encoding': + return '\u0000accept-encoding'; + case 'Accept-Language': + case 'accept-language': + return '\u0000accept-language'; + case 'X-Forwarded-For': + case 'x-forwarded-for': + return '\u0000x-forwarded-for'; + case 'X-Forwarded-Host': + case 'x-forwarded-host': + return '\u0000x-forwarded-host'; + case 'X-Forwarded-Proto': + case 'x-forwarded-proto': + return '\u0000x-forwarded-proto'; + default: + if (low) + return '\u0000' + field; + field = field.toLowerCase(); + low = true; + } + } +} // Add the given (field, value) pair to the message // // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the @@ -128,51 +268,27 @@ function _addHeaderLines(headers, n) { // always joined. IncomingMessage.prototype._addHeaderLine = _addHeaderLine; function _addHeaderLine(field, value, dest) { - field = field.toLowerCase(); - switch (field) { - // Array headers: - case 'set-cookie': - if (dest[field] !== undefined) { - dest[field].push(value); - } else { - dest[field] = [value]; - } - break; - - /* eslint-disable max-len */ - // list is taken from: - // https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp - /* eslint-enable max-len */ - case 'content-type': - case 'content-length': - case 'user-agent': - case 'referer': - case 'host': - case 'authorization': - case 'proxy-authorization': - case 'if-modified-since': - case 'if-unmodified-since': - case 'from': - case 'location': - case 'max-forwards': - case 'retry-after': - case 'etag': - case 'last-modified': - case 'server': - case 'age': - case 'expires': - // drop duplicates - if (dest[field] === undefined) - dest[field] = value; - break; - - default: - // make comma-separated list - if (typeof dest[field] === 'string') { - dest[field] += ', ' + value; - } else { - dest[field] = value; - } + field = matchKnownFields(field); + var flag = field.charCodeAt(0); + if (flag === 0) { + field = field.slice(1); + // Make comma-separated list + if (typeof dest[field] === 'string') { + dest[field] += ', ' + value; + } else { + dest[field] = value; + } + } else if (flag === 1) { + // Array header -- only Set-Cookie at the moment + if (dest['set-cookie'] !== undefined) { + dest['set-cookie'].push(value); + } else { + dest['set-cookie'] = [value]; + } + } else { + // Drop duplicates + if (dest[field] === undefined) + dest[field] = value; } } diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index bf018d988d7abb..8520328683ea47 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -11,26 +11,12 @@ const checkIsHttpToken = common._checkIsHttpToken; const checkInvalidHeaderChar = common._checkInvalidHeaderChar; const CRLF = common.CRLF; -const trfrEncChunkExpression = common.chunkExpression; const debug = common.debug; -const upgradeExpression = /^Upgrade$/i; -const transferEncodingExpression = /^Transfer-Encoding$/i; -const contentLengthExpression = /^Content-Length$/i; -const dateExpression = /^Date$/i; -const expectExpression = /^Expect$/i; -const trailerExpression = /^Trailer$/i; -const connectionExpression = /^Connection$/i; -const connCloseExpression = /(^|\W)close(\W|$)/i; -const connUpgradeExpression = /(^|\W)upgrade(\W|$)/i; - -const automaticHeaders = { - connection: true, - 'content-length': true, - 'transfer-encoding': true, - date: true -}; - +var RE_FIELDS = new RegExp('^(?:Connection|Transfer-Encoding|Content-Length|' + + 'Date|Expect|Trailer|Upgrade)$', 'i'); +var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig; +var RE_TE_CHUNKED = common.chunkExpression; var dateCache; function utcDate() { @@ -70,7 +56,9 @@ function OutgoingMessage() { this.shouldKeepAlive = true; this.useChunkedEncodingByDefault = true; this.sendDate = false; - this._removedHeader = {}; + this._removedConnection = false; + this._removedContLen = false; + this._removedTE = false; this._contentLength = null; this._hasBody = true; @@ -83,7 +71,6 @@ function OutgoingMessage() { this.connection = null; this._header = null; this._headers = null; - this._headerNames = {}; this._onPendingData = null; } @@ -149,36 +136,32 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback) { OutgoingMessage.prototype._writeRaw = _writeRaw; function _writeRaw(data, encoding, callback) { + const conn = this.connection; + if (conn && conn.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; + } + if (typeof encoding === 'function') { callback = encoding; encoding = null; } - var connection = this.connection; - if (connection && - connection._httpMessage === this && - connection.writable && - !connection.destroyed) { + if (conn && conn._httpMessage === this && conn.writable && !conn.destroyed) { // There might be pending data in the this.output buffer. - var outputLength = this.output.length; - if (outputLength > 0) { - this._flushOutput(connection); - } else if (data.length === 0) { + if (this.output.length) { + this._flushOutput(conn); + } else if (!data.length) { if (typeof callback === 'function') process.nextTick(callback); return true; } - // Directly write to socket. - return connection.write(data, encoding, callback); - } else if (connection && 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. - return this._buffer(data, encoding, callback); + return conn.write(data, encoding, callback); } + // Buffer, as long as we're not destroyed. + return this._buffer(data, encoding, callback); } @@ -198,57 +181,72 @@ function _storeHeader(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, - sentConnectionUpgrade: false, - sentContentLengthHeader: false, - sentTransferEncodingHeader: false, - sentDateHeader: false, - sentExpect: false, - sentTrailer: false, - sentUpgrade: false, - messageHeader: firstLine + connection: false, + connUpgrade: false, + contLen: false, + te: false, + date: false, + expect: false, + trailer: false, + upgrade: false, + header: firstLine }; - var i; - var j; var field; + var key; var value; - if (headers instanceof Array) { - for (i = 0; i < headers.length; ++i) { + var i; + var j; + if (headers === this._headers) { + for (key in headers) { + var entry = headers[key]; + field = entry[0]; + value = entry[1]; + + if (value instanceof Array) { + for (j = 0; j < value.length; j++) { + storeHeader(this, state, field, value[j], false); + } + } else { + storeHeader(this, state, field, value, false); + } + } + } else if (headers instanceof Array) { + for (i = 0; i < headers.length; i++) { field = headers[i][0]; value = headers[i][1]; if (value instanceof Array) { for (j = 0; j < value.length; j++) { - storeHeader(this, state, field, value[j]); + storeHeader(this, state, field, value[j], true); } } else { - storeHeader(this, state, field, value); + storeHeader(this, state, field, value, true); } } } else if (headers) { var keys = Object.keys(headers); - for (i = 0; i < keys.length; ++i) { + for (i = 0; i < keys.length; i++) { field = keys[i]; value = headers[field]; if (value instanceof Array) { for (j = 0; j < value.length; j++) { - storeHeader(this, state, field, value[j]); + storeHeader(this, state, field, value[j], true); } } else { - storeHeader(this, state, field, value); + storeHeader(this, state, field, value, true); } } } // Are we upgrading the connection? - if (state.sentConnectionUpgrade && state.sentUpgrade) + if (state.connUpgrade && state.upgrade) this.upgrading = true; // Date header - if (this.sendDate && !state.sentDateHeader) { - state.messageHeader += 'Date: ' + utcDate() + CRLF; + if (this.sendDate && !state.date) { + state.header += 'Date: ' + utcDate() + CRLF; } // Force the connection to close when the response is a 204 No Content or @@ -271,36 +269,33 @@ function _storeHeader(firstLine, headers) { } // keep-alive logic - if (this._removedHeader.connection) { + if (this._removedConnection) { this._last = true; this.shouldKeepAlive = false; - } else if (!state.sentConnectionHeader) { + } else if (!state.connection) { var shouldSendKeepAlive = this.shouldKeepAlive && - (state.sentContentLengthHeader || - this.useChunkedEncodingByDefault || - this.agent); + (state.contLen || this.useChunkedEncodingByDefault || this.agent); if (shouldSendKeepAlive) { - state.messageHeader += 'Connection: keep-alive\r\n'; + state.header += 'Connection: keep-alive\r\n'; } else { this._last = true; - state.messageHeader += 'Connection: close\r\n'; + state.header += 'Connection: close\r\n'; } } - if (!state.sentContentLengthHeader && !state.sentTransferEncodingHeader) { + if (!state.contLen && !state.te) { if (!this._hasBody) { // Make sure we don't end the 0\r\n\r\n at the end of the message. this.chunkedEncoding = false; } else if (!this.useChunkedEncodingByDefault) { this._last = true; } else { - if (!state.sentTrailer && - !this._removedHeader['content-length'] && + if (!state.trailer && + !this._removedContLen && typeof this._contentLength === 'number') { - state.messageHeader += 'Content-Length: ' + this._contentLength + - '\r\n'; - } else if (!this._removedHeader['transfer-encoding']) { - state.messageHeader += 'Transfer-Encoding: chunked\r\n'; + state.header += 'Content-Length: ' + this._contentLength + CRLF; + } else if (!this._removedTE) { + state.header += 'Transfer-Encoding: chunked\r\n'; this.chunkedEncoding = true; } else { // We should only be able to get here if both Content-Length and @@ -311,73 +306,109 @@ function _storeHeader(firstLine, headers) { } } - this._header = state.messageHeader + CRLF; + this._header = state.header + 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(''); + if (state.expect) this._send(''); } -function storeHeader(self, state, field, value) { - if (!checkIsHttpToken(field)) { - throw new TypeError( - 'Header name must be a valid HTTP Token ["' + field + '"]'); - } - if (checkInvalidHeaderChar(value)) { - debug('Header "%s" contains invalid characters', field); - throw new TypeError('The header content contains invalid characters'); - } - state.messageHeader += field + ': ' + escapeHeaderValue(value) + CRLF; - - if (connectionExpression.test(field)) { - state.sentConnectionHeader = true; - if (connCloseExpression.test(value)) { - self._last = true; - } else { - self.shouldKeepAlive = true; +function storeHeader(self, state, field, value, validate) { + if (validate) { + if (!checkIsHttpToken(field)) { + throw new TypeError( + 'Header name must be a valid HTTP Token ["' + field + '"]'); + } + if (value === undefined) { + throw new Error('Header "%s" value must not be undefined', field); + } else if (checkInvalidHeaderChar(value)) { + debug('Header "%s" contains invalid characters', field); + throw new TypeError('The header content contains invalid characters'); } - if (connUpgradeExpression.test(value)) - state.sentConnectionUpgrade = true; - } else if (transferEncodingExpression.test(field)) { - state.sentTransferEncodingHeader = true; - if (trfrEncChunkExpression.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; - } else if (trailerExpression.test(field)) { - state.sentTrailer = true; - } else if (upgradeExpression.test(field)) { - state.sentUpgrade = true; } + state.header += field + ': ' + escapeHeaderValue(value) + CRLF; + matchHeader(self, state, field, value); } +function matchConnValue(self, state, value) { + var sawClose = false; + var m = RE_CONN_VALUES.exec(value); + while (m) { + if (m[0].length === 5) + sawClose = true; + else + state.connUpgrade = true; + m = RE_CONN_VALUES.exec(value); + } + if (sawClose) + self._last = true; + else + self.shouldKeepAlive = true; +} -OutgoingMessage.prototype.setHeader = function setHeader(name, value) { +function matchHeader(self, state, field, value) { + var m = RE_FIELDS.exec(field); + if (!m) + return; + var len = m[0].length; + if (len === 10) { + state.connection = true; + matchConnValue(self, state, value); + } else if (len === 17) { + state.te = true; + if (RE_TE_CHUNKED.test(value)) self.chunkedEncoding = true; + } else if (len === 14) { + state.contLen = true; + } else if (len === 4) { + state.date = true; + } else if (len === 6) { + state.expect = true; + } else if (len === 7) { + var ch = m[0].charCodeAt(0); + if (ch === 85 || ch === 117) + state.upgrade = true; + else + state.trailer = true; + } +} + +function validateHeader(msg, name, value) { if (!checkIsHttpToken(name)) throw new TypeError( 'Header name must be a valid HTTP Token ["' + name + '"]'); if (value === undefined) throw new Error('"value" required in setHeader("' + name + '", value)'); - if (this._header) + if (msg._header) throw new Error('Can\'t set headers after they are sent.'); if (checkInvalidHeaderChar(value)) { debug('Header "%s" contains invalid characters', name); throw new TypeError('The header content contains invalid characters'); } - if (this._headers === null) - this._headers = {}; +} +OutgoingMessage.prototype.setHeader = function setHeader(name, value) { + validateHeader(this, name, value); - var key = name.toLowerCase(); - this._headers[key] = value; - this._headerNames[key] = name; + if (!this._headers) + this._headers = {}; - if (automaticHeaders[key]) - this._removedHeader[key] = false; + const key = name.toLowerCase(); + this._headers[key] = [name, value]; + + switch (key.length) { + case 10: + if (key === 'connection') + this._removedConnection = false; + break; + case 14: + if (key === 'content-length') + this._removedContLen = false; + break; + case 17: + if (key === 'transfer-encoding') + this._removedTE = false; + break; + } }; @@ -388,7 +419,10 @@ OutgoingMessage.prototype.getHeader = function getHeader(name) { if (!this._headers) return; - return this._headers[name.toLowerCase()]; + var entry = this._headers[name.toLowerCase()]; + if (!entry) + return; + return entry[1]; }; @@ -403,37 +437,31 @@ OutgoingMessage.prototype.removeHeader = function removeHeader(name) { var key = name.toLowerCase(); - if (key === 'date') - this.sendDate = false; - else if (automaticHeaders[key]) - this._removedHeader[key] = true; + switch (key.length) { + case 10: + if (key === 'connection') + this._removedConnection = true; + break; + case 14: + if (key === 'content-length') + this._removedContLen = true; + break; + case 17: + if (key === 'transfer-encoding') + this._removedTE = true; + break; + case 4: + if (key === 'date') + this.sendDate = false; + break; + } if (this._headers) { delete this._headers[key]; - delete this._headerNames[key]; } }; -OutgoingMessage.prototype._renderHeaders = function _renderHeaders() { - if (this._header) { - throw new Error('Can\'t render headers after they are sent to the client'); - } - - var headersMap = this._headers; - if (!headersMap) return {}; - - var headers = {}; - var keys = Object.keys(headersMap); - var headerNames = this._headerNames; - - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - headers[headerNames[key]] = headersMap[key]; - } - return headers; -}; - OutgoingMessage.prototype._implicitHeader = function _implicitHeader() { throw new Error('_implicitHeader() method is not implemented'); }; @@ -445,6 +473,7 @@ Object.defineProperty(OutgoingMessage.prototype, 'headersSent', { }); +const crlf_buf = Buffer.from('\r\n'); OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { if (this.finished) { var err = new Error('write after end'); @@ -492,6 +521,7 @@ OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { this.connection.cork(); process.nextTick(connectionCorkNT, this.connection); } + this._send(len.toString(16), 'latin1', null); this._send(crlf_buf, null, null); this._send(chunk, encoding, null); @@ -550,9 +580,6 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { } }; - -const crlf_buf = Buffer.from('\r\n'); - function onFinish(outmsg) { outmsg.emit('finish'); } diff --git a/lib/_http_server.js b/lib/_http_server.js index bd726f83e3e638..be55784e7ec37d 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -161,46 +161,52 @@ ServerResponse.prototype._implicitHeader = function _implicitHeader() { ServerResponse.prototype.writeHead = writeHead; function writeHead(statusCode, reason, obj) { - var headers; + statusCode |= 0; + if (statusCode < 100 || statusCode > 999) + throw new RangeError(`Invalid status code: ${statusCode}`); if (typeof reason === 'string') { // writeHead(statusCode, reasonPhrase[, headers]) this.statusMessage = reason; } else { // writeHead(statusCode[, headers]) - this.statusMessage = - this.statusMessage || STATUS_CODES[statusCode] || 'unknown'; + if (!this.statusMessage) + this.statusMessage = STATUS_CODES[statusCode] || 'unknown'; obj = reason; } this.statusCode = statusCode; + var headers; if (this._headers) { // Slow-case: when progressive API and header fields are passed. + var k; if (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { - var k = keys[i]; + k = keys[i]; if (k) this.setHeader(k, obj[k]); } } + if (k === undefined) { + if (this._header) { + throw new Error('Can\'t render headers after they are sent to the ' + + 'client'); + } + } // only progressive api is used - headers = this._renderHeaders(); + headers = this._headers; } else { // only writeHead() called headers = obj; } - statusCode |= 0; - if (statusCode < 100 || statusCode > 999) - throw new RangeError(`Invalid status code: ${statusCode}`); - if (common._checkInvalidHeaderChar(this.statusMessage)) throw new Error('Invalid character in statusMessage.'); var statusLine = 'HTTP/1.1 ' + statusCode + ' ' + this.statusMessage + CRLF; if (statusCode === 204 || statusCode === 304 || - (100 <= statusCode && statusCode <= 199)) { + (statusCode >= 100 && statusCode <= 199)) { // RFC 2616, 10.2.5: // The 204 response MUST NOT include a message-body, and thus is always // terminated by the first empty line after the header fields. @@ -508,9 +514,8 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { // so that we don't become overwhelmed by a flood of // pipelined requests that may never be resolved. if (!socket._paused) { - var needPause = socket._writableState.needDrain || - state.outgoingData >= socket._writableState.highWaterMark; - if (needPause) { + var ws = socket._writableState; + if (ws.needDrain || state.outgoingData >= ws.highWaterMark) { socket._paused = true; // We also need to pause the parser, but don't do that until after // the call to execute, because we may still be processing the last @@ -536,9 +541,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { // When we're finished writing the response, check if this is the last // response, if so destroy the socket. - var finish = - resOnFinish.bind(undefined, req, res, socket, state); - res.on('finish', finish); + res.on('finish', resOnFinish.bind(undefined, req, res, socket, state)); if (req.headers.expect !== undefined && (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js index ba56225d974fe9..af97fc2d8180c6 100644 --- a/lib/_stream_writable.js +++ b/lib/_stream_writable.js @@ -16,13 +16,6 @@ util.inherits(Writable, Stream); function nop() {} -function WriteReq(chunk, encoding, cb) { - this.chunk = chunk; - this.encoding = encoding; - this.callback = cb; - this.next = null; -} - function WritableState(options, stream) { options = options || {}; @@ -113,7 +106,9 @@ function WritableState(options, stream) { // allocate the first CorkedRequest, there is always // one allocated and free to use, and we maintain at most two - this.corkedRequestsFree = new CorkedRequest(this); + var corkReq = { next: null, entry: null, finish: undefined }; + corkReq.finish = onCorkedFinish.bind(undefined, corkReq, this); + this.corkedRequestsFree = corkReq; } WritableState.prototype.getBuffer = function getBuffer() { @@ -289,9 +284,11 @@ function decodeChunk(state, chunk, encoding) { // If we return false, then we need a drain event, so set that flag. function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { if (!isBuf) { - chunk = decodeChunk(state, chunk, encoding); - if (chunk instanceof Buffer) + var newChunk = decodeChunk(state, chunk, encoding); + if (chunk !== newChunk) { encoding = 'buffer'; + chunk = newChunk; + } } var len = state.objectMode ? 1 : chunk.length; @@ -304,7 +301,7 @@ function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { if (state.writing || state.corked) { var last = state.lastBufferedRequest; - state.lastBufferedRequest = new WriteReq(chunk, encoding, cb); + state.lastBufferedRequest = { chunk, encoding, callback: cb, next: null }; if (last) { last.next = state.lastBufferedRequest; } else { @@ -423,7 +420,9 @@ function clearBuffer(stream, state) { state.corkedRequestsFree = holder.next; holder.next = null; } else { - state.corkedRequestsFree = new CorkedRequest(state); + var corkReq = { next: null, entry: null, finish: undefined }; + corkReq.finish = onCorkedFinish.bind(undefined, corkReq, state); + state.corkedRequestsFree = corkReq; } } else { // Slow case, write chunks one-by-one @@ -528,14 +527,6 @@ function endWritable(stream, state, cb) { stream.writable = false; } -// It seems a linked list but it is not -// there will be only 2 of these for each stream -function CorkedRequest(state) { - this.next = null; - this.entry = null; - this.finish = onCorkedFinish.bind(undefined, this, state); -} - function onCorkedFinish(corkReq, state, err) { var entry = corkReq.entry; corkReq.entry = null;