nodejs/node · #66257
Speed up HTTP header materialization
benchmark/http/incoming_headers.js8 + / 4 −
@@ -6,16 +6,20 @@ const bench = common.createBenchmark(main, { connections: [50], // Concurrent connections headers: [20], // Number of header lines to append after the common headers w: [0, 6], // Amount of trailing whitespace+ read: [0, 1], // Whether the handler reads req.headers duration: 5, }); -function main({ connections, headers, w, duration }) {+function main({ connections, headers, w, read, duration }) { const server = http.createServer((req, res) => {+ if (read && req.headers.host === undefined) {+ throw new Error('Missing Host header');+ } res.end(); }); server.listen(0, () => {- const headers = {+ const requestHeaders = { 'Content-Type': 'text/plain', 'Accept': 'text/plain', 'User-Agent': 'nodejs-benchmark',@@ -28,12 +32,12 @@ function main({ connections, headers, w, duration }) { // - wrk can only send trailing OWS. This is a side-effect of wrk // processing requests with http-parser before sending them, causing // leading OWS to be stripped.- headers[`foo${i}`] = `some header value ${i}${' \t'.repeat(w / 2)}`;+ requestHeaders[`foo${i}`] = `some header value ${i}${' \t'.repeat(w / 2)}`; } bench.http({ path: '/', connections,- headers,+ headers: requestHeaders, duration, port: server.address().port, }, () => {lib/_http_incoming.js109 + / 53 −
@@ -341,109 +341,161 @@ 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().+// How repeated instances of a header field are combined in the headers+// object. Fields that are not known are joined with ', '.+const kFirstWins = 0;+const kJoinComma = 1;+const kJoinSemicolon = 2;+const kArray = 3;++function knownField(name, merge) {+ return { name, merge };+}++// Later values are dropped, unless joinDuplicateHeaders is set.+const kAge = knownField('age', kFirstWins);+const kHost = knownField('host', kFirstWins);+const kFrom = knownField('from', kFirstWins);+const kETag = knownField('etag', kFirstWins);+const kServer = knownField('server', kFirstWins);+const kReferer = knownField('referer', kFirstWins);+const kExpires = knownField('expires', kFirstWins);+const kLocation = knownField('location', kFirstWins);+const kUserAgent = knownField('user-agent', kFirstWins);+const kRetryAfter = knownField('retry-after', kFirstWins);+const kContentType = knownField('content-type', kFirstWins);+const kMaxForwards = knownField('max-forwards', kFirstWins);+const kAuthorization = knownField('authorization', kFirstWins);+const kLastModified = knownField('last-modified', kFirstWins);+const kContentLength = knownField('content-length', kFirstWins);+const kIfModifiedSince = knownField('if-modified-since', kFirstWins);+const kProxyAuthorization = knownField('proxy-authorization', kFirstWins);+const kIfUnmodifiedSince = knownField('if-unmodified-since', kFirstWins);++// Values are joined with ', '.+const kDate = knownField('date', kJoinComma);+const kVary = knownField('vary', kJoinComma);+const kOrigin = knownField('origin', kJoinComma);+const kExpect = knownField('expect', kJoinComma);+const kAccept = knownField('accept', kJoinComma);+const kUpgrade = knownField('upgrade', kJoinComma);+const kIfMatch = knownField('if-match', kJoinComma);+const kConnection = knownField('connection', kJoinComma);+const kCacheControl = knownField('cache-control', kJoinComma);+const kIfNoneMatch = knownField('if-none-match', kJoinComma);+const kAcceptEncoding = knownField('accept-encoding', kJoinComma);+const kAcceptLanguage = knownField('accept-language', kJoinComma);+const kXForwardedFor = knownField('x-forwarded-for', kJoinComma);+const kContentEncoding = knownField('content-encoding', kJoinComma);+const kXForwardedHost = knownField('x-forwarded-host', kJoinComma);+const kTransferEncoding = knownField('transfer-encoding', kJoinComma);+const kXForwardedProto = knownField('x-forwarded-proto', kJoinComma);++// Values are joined with '; '.+const kCookie = knownField('cookie', kJoinSemicolon);++// Values are collected into an array.+const kSetCookie = knownField('set-cookie', kArray);++// Returns the descriptor of a known field, or the lowercased name of any other+// field. The 'traditional cased' and lowercase spellings of known fields are+// matched first to avoid calling toLowerCase() for them. // 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. function matchKnownFields(field, lowercased) { switch (field.length) { case 3:- if (field === 'Age' || field === 'age') return 'age';+ if (field === 'Age' || field === 'age') return kAge; break; case 4:- if (field === 'Host' || field === 'host') return 'host';- if (field === 'From' || field === 'from') return 'from';- if (field === 'ETag' || field === 'etag') return 'etag';- if (field === 'Date' || field === 'date') return '\u0000date';- if (field === 'Vary' || field === 'vary') return '\u0000vary';+ if (field === 'Host' || field === 'host') return kHost;+ if (field === 'From' || field === 'from') return kFrom;+ if (field === 'ETag' || field === 'etag') return kETag;+ if (field === 'Date' || field === 'date') return kDate;+ if (field === 'Vary' || field === 'vary') return kVary; break; case 6:- if (field === 'Server' || field === 'server') return 'server';- if (field === 'Cookie' || field === 'cookie') return '\u0002cookie';- if (field === 'Origin' || field === 'origin') return '\u0000origin';- if (field === 'Expect' || field === 'expect') return '\u0000expect';- if (field === 'Accept' || field === 'accept') return '\u0000accept';+ if (field === 'Server' || field === 'server') return kServer;+ if (field === 'Cookie' || field === 'cookie') return kCookie;+ if (field === 'Origin' || field === 'origin') return kOrigin;+ if (field === 'Expect' || field === 'expect') return kExpect;+ if (field === 'Accept' || field === 'accept') return kAccept; break; case 7:- if (field === 'Referer' || field === 'referer') return 'referer';- if (field === 'Expires' || field === 'expires') return 'expires';- if (field === 'Upgrade' || field === 'upgrade') return '\u0000upgrade';+ if (field === 'Referer' || field === 'referer') return kReferer;+ if (field === 'Expires' || field === 'expires') return kExpires;+ if (field === 'Upgrade' || field === 'upgrade') return kUpgrade; break; case 8: if (field === 'Location' || field === 'location')- return 'location';+ return kLocation; if (field === 'If-Match' || field === 'if-match')- return '\u0000if-match';+ return kIfMatch; break; case 10: if (field === 'User-Agent' || field === 'user-agent')- return 'user-agent';+ return kUserAgent; if (field === 'Set-Cookie' || field === 'set-cookie')- return '\u0001';+ return kSetCookie; if (field === 'Connection' || field === 'connection')- return '\u0000connection';+ return kConnection; break; case 11: if (field === 'Retry-After' || field === 'retry-after')- return 'retry-after';+ return kRetryAfter; break; case 12: if (field === 'Content-Type' || field === 'content-type')- return 'content-type';+ return kContentType; if (field === 'Max-Forwards' || field === 'max-forwards')- return 'max-forwards';+ return kMaxForwards; break; case 13: if (field === 'Authorization' || field === 'authorization')- return 'authorization';+ return kAuthorization; if (field === 'Last-Modified' || field === 'last-modified')- return 'last-modified';+ return kLastModified; if (field === 'Cache-Control' || field === 'cache-control')- return '\u0000cache-control';+ return kCacheControl; if (field === 'If-None-Match' || field === 'if-none-match')- return '\u0000if-none-match';+ return kIfNoneMatch; break; case 14: if (field === 'Content-Length' || field === 'content-length')- return 'content-length';+ return kContentLength; break; case 15: if (field === 'Accept-Encoding' || field === 'accept-encoding')- return '\u0000accept-encoding';+ return kAcceptEncoding; if (field === 'Accept-Language' || field === 'accept-language')- return '\u0000accept-language';+ return kAcceptLanguage; if (field === 'X-Forwarded-For' || field === 'x-forwarded-for')- return '\u0000x-forwarded-for';+ return kXForwardedFor; break; case 16: if (field === 'Content-Encoding' || field === 'content-encoding')- return '\u0000content-encoding';+ return kContentEncoding; if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host')- return '\u0000x-forwarded-host';+ return kXForwardedHost; break; case 17: if (field === 'If-Modified-Since' || field === 'if-modified-since')- return 'if-modified-since';+ return kIfModifiedSince; if (field === 'Transfer-Encoding' || field === 'transfer-encoding')- return '\u0000transfer-encoding';+ return kTransferEncoding; if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto')- return '\u0000x-forwarded-proto';+ return kXForwardedProto; break; case 19: if (field === 'Proxy-Authorization' || field === 'proxy-authorization')- return 'proxy-authorization';+ return kProxyAuthorization; if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since')- return 'if-unmodified-since';+ return kIfUnmodifiedSince; break; } if (lowercased) {- return '\u0000' + field;+ return field; } return matchKnownFields(field.toLowerCase(), true); }@@ -454,21 +506,25 @@ function matchKnownFields(field, lowercased) { // multiple values this way. The one exception to this is the Cookie header, // which has multiple values joined with a '; ' instead. If a header's values // cannot be joined in either of these ways, we declare the first instance the-// winner and drop the second. Extended header fields (those beginning with-// 'x-') are always joined.+// winner and drop the second. Fields that are not known are always joined. IncomingMessage.prototype._addHeaderLine = _addHeaderLine; function _addHeaderLine(field, value, dest) {- field = matchKnownFields(field);- const flag = field.charCodeAt(0);- if (flag === 0 || flag === 2) {- field = field.slice(1);+ const known = matchKnownFields(field);+ let merge = kJoinComma;+ if (typeof known === 'string') {+ field = known;+ } else {+ field = known.name;+ merge = known.merge;+ }+ if (merge === kJoinComma || merge === kJoinSemicolon) { // Make a delimited list if (typeof dest[field] === 'string') {- dest[field] += (flag === 0 ? ', ' : '; ') + value;+ dest[field] += (merge === kJoinComma ? ', ' : '; ') + value; } else { dest[field] = value; }- } else if (flag === 1) {+ } else if (merge === kArray) { // Array header -- only Set-Cookie at the moment if (dest['set-cookie'] !== undefined) { dest['set-cookie'].push(value);test/parallel/test-http-incoming-matchKnownFields.js18 + / 0 −
@@ -91,3 +91,21 @@ checkDest('X-Forwarded-Proto', { 'x-forwarded-proto': undefined }); checkDest('x-forwarded-proto', { 'x-forwarded-proto': 'test, value' }, 'value'); checkDest('X-Foo', { 'x-foo': undefined }); checkDest('x-foo', { 'x-foo': 'test, value' }, 'value');++// Known fields in other casings are merged by the same rules as their usual+// spellings.+checkDest('CONTENT-TYPE', { 'content-type': 'test' }, 'value');+checkDest('CONNECTION', { connection: 'test, value' }, 'value');+checkDest('COOKIE', { cookie: 'test; value' }, 'value');+checkDest('SET-COOKIE', { 'set-cookie': ['test', 'value'] }, 'value');+checkDest('X-FOO', { 'x-foo': 'test, value' }, 'value');++// joinDuplicateHeaders also applies to first-wins fields in other casings.+{+ const incomingMessage = new IncomingMessage();+ incomingMessage.joinDuplicateHeaders = true;+ const dest = {};+ incomingMessage._addHeaderLine('AUTHORIZATION', 'a', dest);+ incomingMessage._addHeaderLine('Authorization', 'b', dest);+ assert.deepStrictEqual(dest, { authorization: 'a, b' });+}