Skip to content

Commit 45f853a

Browse files
pimterryaduh95
authored andcommitted
benchmark: fix shadowing that broke h=20 on incoming_headers benchmark
Previously the inner 'headers' variable shadowed the outer, meaning that headers=20 sends the same 7 headers as headers=0. Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: #66257 Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent d748bb7 commit 45f853a

3 files changed

Lines changed: 135 additions & 57 deletions

File tree

‎benchmark/http/incoming_headers.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ const bench = common.createBenchmark(main, {
66
connections: [50], // Concurrent connections
77
headers: [20], // Number of header lines to append after the common headers
88
w: [0, 6], // Amount of trailing whitespace
9+
read: [0, 1], // Whether the handler reads req.headers
910
duration: 5,
1011
});
1112

12-
function main({ connections, headers, w, duration }) {
13+
function main({ connections, headers, w, read, duration }) {
1314
const server = http.createServer((req, res) => {
15+
if (read && req.headers.host === undefined) {
16+
throw new Error('Missing Host header');
17+
}
1418
res.end();
1519
});
1620

1721
server.listen(0, () => {
18-
const headers = {
22+
const requestHeaders = {
1923
'Content-Type': 'text/plain',
2024
'Accept': 'text/plain',
2125
'User-Agent': 'nodejs-benchmark',
@@ -28,12 +32,12 @@ function main({ connections, headers, w, duration }) {
2832
// - wrk can only send trailing OWS. This is a side-effect of wrk
2933
// processing requests with http-parser before sending them, causing
3034
// leading OWS to be stripped.
31-
headers[`foo${i}`] = `some header value ${i}${' \t'.repeat(w / 2)}`;
35+
requestHeaders[`foo${i}`] = `some header value ${i}${' \t'.repeat(w / 2)}`;
3236
}
3337
bench.http({
3438
path: '/',
3539
connections,
36-
headers,
40+
headers: requestHeaders,
3741
duration,
3842
port: server.address().port,
3943
}, () => {

‎lib/_http_incoming.js‎

Lines changed: 109 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -334,109 +334,161 @@ function _addHeaderLines(headers, n) {
334334
}
335335

336336

337-
// This function is used to help avoid the lowercasing of a field name if it
338-
// matches a 'traditional cased' version of a field name. It then returns the
339-
// lowercased name to both avoid calling toLowerCase() a second time and to
340-
// indicate whether the field was a 'no duplicates' field. If a field is not a
341-
// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception
342-
// to this is the Set-Cookie header which is indicated by a `1` byte flag, since
343-
// it is an 'array' field and thus is treated differently in _addHeaderLines().
337+
// How repeated instances of a header field are combined in the headers
338+
// object. Fields that are not known are joined with ', '.
339+
const kFirstWins = 0;
340+
const kJoinComma = 1;
341+
const kJoinSemicolon = 2;
342+
const kArray = 3;
343+
344+
function knownField(name, merge) {
345+
return { name, merge };
346+
}
347+
348+
// Later values are dropped, unless joinDuplicateHeaders is set.
349+
const kAge = knownField('age', kFirstWins);
350+
const kHost = knownField('host', kFirstWins);
351+
const kFrom = knownField('from', kFirstWins);
352+
const kETag = knownField('etag', kFirstWins);
353+
const kServer = knownField('server', kFirstWins);
354+
const kReferer = knownField('referer', kFirstWins);
355+
const kExpires = knownField('expires', kFirstWins);
356+
const kLocation = knownField('location', kFirstWins);
357+
const kUserAgent = knownField('user-agent', kFirstWins);
358+
const kRetryAfter = knownField('retry-after', kFirstWins);
359+
const kContentType = knownField('content-type', kFirstWins);
360+
const kMaxForwards = knownField('max-forwards', kFirstWins);
361+
const kAuthorization = knownField('authorization', kFirstWins);
362+
const kLastModified = knownField('last-modified', kFirstWins);
363+
const kContentLength = knownField('content-length', kFirstWins);
364+
const kIfModifiedSince = knownField('if-modified-since', kFirstWins);
365+
const kProxyAuthorization = knownField('proxy-authorization', kFirstWins);
366+
const kIfUnmodifiedSince = knownField('if-unmodified-since', kFirstWins);
367+
368+
// Values are joined with ', '.
369+
const kDate = knownField('date', kJoinComma);
370+
const kVary = knownField('vary', kJoinComma);
371+
const kOrigin = knownField('origin', kJoinComma);
372+
const kExpect = knownField('expect', kJoinComma);
373+
const kAccept = knownField('accept', kJoinComma);
374+
const kUpgrade = knownField('upgrade', kJoinComma);
375+
const kIfMatch = knownField('if-match', kJoinComma);
376+
const kConnection = knownField('connection', kJoinComma);
377+
const kCacheControl = knownField('cache-control', kJoinComma);
378+
const kIfNoneMatch = knownField('if-none-match', kJoinComma);
379+
const kAcceptEncoding = knownField('accept-encoding', kJoinComma);
380+
const kAcceptLanguage = knownField('accept-language', kJoinComma);
381+
const kXForwardedFor = knownField('x-forwarded-for', kJoinComma);
382+
const kContentEncoding = knownField('content-encoding', kJoinComma);
383+
const kXForwardedHost = knownField('x-forwarded-host', kJoinComma);
384+
const kTransferEncoding = knownField('transfer-encoding', kJoinComma);
385+
const kXForwardedProto = knownField('x-forwarded-proto', kJoinComma);
386+
387+
// Values are joined with '; '.
388+
const kCookie = knownField('cookie', kJoinSemicolon);
389+
390+
// Values are collected into an array.
391+
const kSetCookie = knownField('set-cookie', kArray);
392+
393+
// Returns the descriptor of a known field, or the lowercased name of any other
394+
// field. The 'traditional cased' and lowercase spellings of known fields are
395+
// matched first to avoid calling toLowerCase() for them.
344396
// TODO: perhaps http_parser could be returning both raw and lowercased versions
345397
// of known header names to avoid us having to call toLowerCase() for those
346398
// headers.
347399
function matchKnownFields(field, lowercased) {
348400
switch (field.length) {
349401
case 3:
350-
if (field === 'Age' || field === 'age') return 'age';
402+
if (field === 'Age' || field === 'age') return kAge;
351403
break;
352404
case 4:
353-
if (field === 'Host' || field === 'host') return 'host';
354-
if (field === 'From' || field === 'from') return 'from';
355-
if (field === 'ETag' || field === 'etag') return 'etag';
356-
if (field === 'Date' || field === 'date') return '\u0000date';
357-
if (field === 'Vary' || field === 'vary') return '\u0000vary';
405+
if (field === 'Host' || field === 'host') return kHost;
406+
if (field === 'From' || field === 'from') return kFrom;
407+
if (field === 'ETag' || field === 'etag') return kETag;
408+
if (field === 'Date' || field === 'date') return kDate;
409+
if (field === 'Vary' || field === 'vary') return kVary;
358410
break;
359411
case 6:
360-
if (field === 'Server' || field === 'server') return 'server';
361-
if (field === 'Cookie' || field === 'cookie') return '\u0002cookie';
362-
if (field === 'Origin' || field === 'origin') return '\u0000origin';
363-
if (field === 'Expect' || field === 'expect') return '\u0000expect';
364-
if (field === 'Accept' || field === 'accept') return '\u0000accept';
412+
if (field === 'Server' || field === 'server') return kServer;
413+
if (field === 'Cookie' || field === 'cookie') return kCookie;
414+
if (field === 'Origin' || field === 'origin') return kOrigin;
415+
if (field === 'Expect' || field === 'expect') return kExpect;
416+
if (field === 'Accept' || field === 'accept') return kAccept;
365417
break;
366418
case 7:
367-
if (field === 'Referer' || field === 'referer') return 'referer';
368-
if (field === 'Expires' || field === 'expires') return 'expires';
369-
if (field === 'Upgrade' || field === 'upgrade') return '\u0000upgrade';
419+
if (field === 'Referer' || field === 'referer') return kReferer;
420+
if (field === 'Expires' || field === 'expires') return kExpires;
421+
if (field === 'Upgrade' || field === 'upgrade') return kUpgrade;
370422
break;
371423
case 8:
372424
if (field === 'Location' || field === 'location')
373-
return 'location';
425+
return kLocation;
374426
if (field === 'If-Match' || field === 'if-match')
375-
return '\u0000if-match';
427+
return kIfMatch;
376428
break;
377429
case 10:
378430
if (field === 'User-Agent' || field === 'user-agent')
379-
return 'user-agent';
431+
return kUserAgent;
380432
if (field === 'Set-Cookie' || field === 'set-cookie')
381-
return '\u0001';
433+
return kSetCookie;
382434
if (field === 'Connection' || field === 'connection')
383-
return '\u0000connection';
435+
return kConnection;
384436
break;
385437
case 11:
386438
if (field === 'Retry-After' || field === 'retry-after')
387-
return 'retry-after';
439+
return kRetryAfter;
388440
break;
389441
case 12:
390442
if (field === 'Content-Type' || field === 'content-type')
391-
return 'content-type';
443+
return kContentType;
392444
if (field === 'Max-Forwards' || field === 'max-forwards')
393-
return 'max-forwards';
445+
return kMaxForwards;
394446
break;
395447
case 13:
396448
if (field === 'Authorization' || field === 'authorization')
397-
return 'authorization';
449+
return kAuthorization;
398450
if (field === 'Last-Modified' || field === 'last-modified')
399-
return 'last-modified';
451+
return kLastModified;
400452
if (field === 'Cache-Control' || field === 'cache-control')
401-
return '\u0000cache-control';
453+
return kCacheControl;
402454
if (field === 'If-None-Match' || field === 'if-none-match')
403-
return '\u0000if-none-match';
455+
return kIfNoneMatch;
404456
break;
405457
case 14:
406458
if (field === 'Content-Length' || field === 'content-length')
407-
return 'content-length';
459+
return kContentLength;
408460
break;
409461
case 15:
410462
if (field === 'Accept-Encoding' || field === 'accept-encoding')
411-
return '\u0000accept-encoding';
463+
return kAcceptEncoding;
412464
if (field === 'Accept-Language' || field === 'accept-language')
413-
return '\u0000accept-language';
465+
return kAcceptLanguage;
414466
if (field === 'X-Forwarded-For' || field === 'x-forwarded-for')
415-
return '\u0000x-forwarded-for';
467+
return kXForwardedFor;
416468
break;
417469
case 16:
418470
if (field === 'Content-Encoding' || field === 'content-encoding')
419-
return '\u0000content-encoding';
471+
return kContentEncoding;
420472
if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host')
421-
return '\u0000x-forwarded-host';
473+
return kXForwardedHost;
422474
break;
423475
case 17:
424476
if (field === 'If-Modified-Since' || field === 'if-modified-since')
425-
return 'if-modified-since';
477+
return kIfModifiedSince;
426478
if (field === 'Transfer-Encoding' || field === 'transfer-encoding')
427-
return '\u0000transfer-encoding';
479+
return kTransferEncoding;
428480
if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto')
429-
return '\u0000x-forwarded-proto';
481+
return kXForwardedProto;
430482
break;
431483
case 19:
432484
if (field === 'Proxy-Authorization' || field === 'proxy-authorization')
433-
return 'proxy-authorization';
485+
return kProxyAuthorization;
434486
if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since')
435-
return 'if-unmodified-since';
487+
return kIfUnmodifiedSince;
436488
break;
437489
}
438490
if (lowercased) {
439-
return '\u0000' + field;
491+
return field;
440492
}
441493
return matchKnownFields(field.toLowerCase(), true);
442494
}
@@ -447,21 +499,25 @@ function matchKnownFields(field, lowercased) {
447499
// multiple values this way. The one exception to this is the Cookie header,
448500
// which has multiple values joined with a '; ' instead. If a header's values
449501
// cannot be joined in either of these ways, we declare the first instance the
450-
// winner and drop the second. Extended header fields (those beginning with
451-
// 'x-') are always joined.
502+
// winner and drop the second. Fields that are not known are always joined.
452503
IncomingMessage.prototype._addHeaderLine = _addHeaderLine;
453504
function _addHeaderLine(field, value, dest) {
454-
field = matchKnownFields(field);
455-
const flag = field.charCodeAt(0);
456-
if (flag === 0 || flag === 2) {
457-
field = field.slice(1);
505+
const known = matchKnownFields(field);
506+
let merge = kJoinComma;
507+
if (typeof known === 'string') {
508+
field = known;
509+
} else {
510+
field = known.name;
511+
merge = known.merge;
512+
}
513+
if (merge === kJoinComma || merge === kJoinSemicolon) {
458514
// Make a delimited list
459515
if (typeof dest[field] === 'string') {
460-
dest[field] += (flag === 0 ? ', ' : '; ') + value;
516+
dest[field] += (merge === kJoinComma ? ', ' : '; ') + value;
461517
} else {
462518
dest[field] = value;
463519
}
464-
} else if (flag === 1) {
520+
} else if (merge === kArray) {
465521
// Array header -- only Set-Cookie at the moment
466522
if (dest['set-cookie'] !== undefined) {
467523
dest['set-cookie'].push(value);

‎test/parallel/test-http-incoming-matchKnownFields.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,21 @@ checkDest('X-Forwarded-Proto', { 'x-forwarded-proto': undefined });
9191
checkDest('x-forwarded-proto', { 'x-forwarded-proto': 'test, value' }, 'value');
9292
checkDest('X-Foo', { 'x-foo': undefined });
9393
checkDest('x-foo', { 'x-foo': 'test, value' }, 'value');
94+
95+
// Known fields in other casings are merged by the same rules as their usual
96+
// spellings.
97+
checkDest('CONTENT-TYPE', { 'content-type': 'test' }, 'value');
98+
checkDest('CONNECTION', { connection: 'test, value' }, 'value');
99+
checkDest('COOKIE', { cookie: 'test; value' }, 'value');
100+
checkDest('SET-COOKIE', { 'set-cookie': ['test', 'value'] }, 'value');
101+
checkDest('X-FOO', { 'x-foo': 'test, value' }, 'value');
102+
103+
// joinDuplicateHeaders also applies to first-wins fields in other casings.
104+
{
105+
const incomingMessage = new IncomingMessage();
106+
incomingMessage.joinDuplicateHeaders = true;
107+
const dest = {};
108+
incomingMessage._addHeaderLine('AUTHORIZATION', 'a', dest);
109+
incomingMessage._addHeaderLine('Authorization', 'b', dest);
110+
assert.deepStrictEqual(dest, { authorization: 'a, b' });
111+
}

0 commit comments

Comments
 (0)