Commit af135021599 for nodejs

commit af135021599773ce4a6b88850957e684d9c532b4
Author: Tim Perry <pimterry@gmail.com>
Date:   Sat Sep 26 20:27:41 2026 +0200

    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: https://github.com/nodejs/node/pull/66257
    Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>

diff --git a/benchmark/http/incoming_headers.js b/benchmark/http/incoming_headers.js
index 21e15fcd7d6..8a1b8bf7de7 100644
--- a/benchmark/http/incoming_headers.js
+++ b/benchmark/http/incoming_headers.js
@@ -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,
     }, () => {
diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js
index 648b148798b..0aa7d30580e 100644
--- a/lib/_http_incoming.js
+++ b/lib/_http_incoming.js
@@ -340,109 +340,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);
 }
@@ -453,21 +505,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);
diff --git a/test/parallel/test-http-incoming-matchKnownFields.js b/test/parallel/test-http-incoming-matchKnownFields.js
index 4402cc519a8..837dfc7e8f0 100644
--- a/test/parallel/test-http-incoming-matchKnownFields.js
+++ b/test/parallel/test-http-incoming-matchKnownFields.js
@@ -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' });
+}