Commit 4bd56b3590b for nodejs

commit 4bd56b3590b5a1dc10dc2ce8ee1652d3dd657615
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date:   Fri Sep 25 16:47:42 2026 -0400

    http: avoid toLowerCase on the server hot path

    After #65802, Host/Expect/body checks still allocate
    toLowerCase() copies and Expect/HTTP/1.0 TE still
    read req.headers. Compare names without allocating,
    read those values from rawHeaders, intern parser
    header names, and skip Title-Case toLowerCase on
    common outgoing fields.

    Refs: https://github.com/nodejs/node/pull/65802
    Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
    Assisted-by: a closed-source coding agent
    PR-URL: https://github.com/nodejs/node/pull/66120
    Refs: https://github.com/nodejs/node/pull/65332
    Reviewed-By: Robert Nagy <ronagy@icloud.com>
    Reviewed-By: Filip Skokan <panva.ip@gmail.com>

diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js
index 1e7fc8adeef..648b148798b 100644
--- a/lib/_http_incoming.js
+++ b/lib/_http_incoming.js
@@ -63,16 +63,15 @@ function IncomingMessage(socket) {
     return deprecateInstantiation(IncomingMessage, 'DEP0195', socket);
   }

-  let streamOptions;
+  Readable.call(this);

   if (socket) {
-    streamOptions = {
-      highWaterMark: socket.readableHighWaterMark,
-    };
+    const hwm = socket.readableHighWaterMark;
+    if (this._readableState.highWaterMark !== hwm) {
+      this._readableState.highWaterMark = hwm;
+    }
   }

-  Readable.call(this, streamOptions);
-
   this._readableState.readingMore = true;

   this.socket = socket;
@@ -530,6 +529,43 @@ IncomingMessage.prototype._dump = function _dump() {
   }
 };

+// Case-insensitive ASCII compare against an already-lowercased name.
+// Avoids allocating a lowercased copy of every header name.
+function asciiEqualIgnoreCase(a, lower) {
+  const len = lower.length;
+  if (a.length !== len)
+    return false;
+  if (a === lower)
+    return true;
+  for (let i = 0; i < len; i++) {
+    let c = a.charCodeAt(i);
+    if (c >= 65 && c <= 90)
+      c += 32;
+    if (c !== lower.charCodeAt(i))
+      return false;
+  }
+  return true;
+}
+
+function getRawHeader(msg, lowerName, joinDuplicates) {
+  const rawHeaders = msg.rawHeaders;
+  const count = msg[kHeadersCount];
+  let result;
+  for (let i = 0; i < count; i += 2) {
+    if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
+      continue;
+    const value = rawHeaders[i + 1];
+    if (result === undefined) {
+      result = value;
+      if (!joinDuplicates)
+        return result;
+    } else {
+      result += ', ' + value;
+    }
+  }
+  return result;
+}
+
 function onError(self, error, cb) {
   // This is to keep backward compatible behavior.
   // An error is emitted only if there are listeners attached to the event.
@@ -544,6 +580,8 @@ module.exports = {
   IncomingMessage,
   kDetachAbortSignal,
   kHeadersCount,
+  asciiEqualIgnoreCase,
+  getRawHeader,
   readStart,
   readStop,
 };
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
index a9d98279674..0b728f6ae4d 100644
--- a/lib/_http_outgoing.js
+++ b/lib/_http_outgoing.js
@@ -659,10 +659,38 @@ function storeHeader(self, state, key, value, validate, lenient) {
   matchHeader(self, state, key, value);
 }

+function lowerOutgoingHeaderName(field) {
+  switch (field) {
+    case 'Connection':
+    case 'connection':
+      return 'connection';
+    case 'Content-Length':
+    case 'content-length':
+      return 'content-length';
+    case 'Transfer-Encoding':
+    case 'transfer-encoding':
+      return 'transfer-encoding';
+    case 'Date':
+    case 'date':
+      return 'date';
+    case 'Expect':
+    case 'expect':
+      return 'expect';
+    case 'Trailer':
+    case 'trailer':
+      return 'trailer';
+    case 'Keep-Alive':
+    case 'keep-alive':
+      return 'keep-alive';
+    default:
+      return field.toLowerCase();
+  }
+}
+
 function matchHeader(self, state, field, value) {
   if (field.length < 4 || field.length > 17)
     return;
-  field = field.toLowerCase();
+  field = lowerOutgoingHeaderName(field);
   switch (field) {
     case 'connection':
       state.connection = true;
diff --git a/lib/_http_server.js b/lib/_http_server.js
index 64189a4a664..799f5b7c5ac 100644
--- a/lib/_http_server.js
+++ b/lib/_http_server.js
@@ -74,6 +74,8 @@ const {
   IncomingMessage,
   kDetachAbortSignal,
   kHeadersCount,
+  asciiEqualIgnoreCase,
+  getRawHeader,
 } = require('_http_incoming');
 const {
   ConnResetException,
@@ -225,7 +227,8 @@ function ServerResponse(req, options) {
   this._expect_continue = false;

   if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
-    this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
+    this.useChunkedEncodingByDefault =
+      chunkExpression.test(getRawHeader(req, 'te', true));
     this.shouldKeepAlive = false;
   }

@@ -1289,10 +1292,8 @@ function hasRequestHeader(req, length, lowerName) {
   const count = req[kHeadersCount];
   for (let i = 0; i < count; i += 2) {
     const key = rawHeaders[i];
-    if (key.length === length &&
-        (key === lowerName || key.toLowerCase() === lowerName)) {
+    if (key.length === length && asciiEqualIgnoreCase(key, lowerName))
       return true;
-    }
   }
   return false;
 }
@@ -1301,12 +1302,14 @@ function hasBodyHeaders(req) {
   const rawHeaders = req.rawHeaders;
   const count = req[kHeadersCount];
   for (let i = 0; i < count; i += 2) {
-    const length = rawHeaders[i].length;
-    if (length === 14 || length === 17) {
-      const key = rawHeaders[i].toLowerCase();
-      if (key === 'content-length' || key === 'transfer-encoding') {
+    const key = rawHeaders[i];
+    const length = key.length;
+    if (length === 14) {
+      if (asciiEqualIgnoreCase(key, 'content-length'))
+        return true;
+    } else if (length === 17) {
+      if (asciiEqualIgnoreCase(key, 'transfer-encoding'))
         return true;
-      }
     }
   }
   return false;
@@ -1441,22 +1444,25 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
       server.emit('dropRequest', req, socket);
       res.writeHead(503);
       res.end();
-    } else if (hasRequestHeader(req, 6, 'expect')) {
-      handled = true;
-
-      if (continueExpression.test(req.headers.expect)) {
-        res._expect_continue = true;
-        if (server.listenerCount('checkContinue') > 0) {
-          server.emit('checkContinue', req, res);
+    } else {
+      const expect = getRawHeader(req, 'expect', true);
+      if (expect !== undefined) {
+        handled = true;
+
+        if (continueExpression.test(expect)) {
+          res._expect_continue = true;
+          if (server.listenerCount('checkContinue') > 0) {
+            server.emit('checkContinue', req, res);
+          } else {
+            res.writeContinue();
+            server.emit('request', req, res);
+          }
+        } else if (server.listenerCount('checkExpectation') > 0) {
+          server.emit('checkExpectation', req, res);
         } else {
-          res.writeContinue();
-          server.emit('request', req, res);
+          res.writeHead(417);
+          res.end();
         }
-      } else if (server.listenerCount('checkExpectation') > 0) {
-        server.emit('checkExpectation', req, res);
-      } else {
-        res.writeHead(417);
-        res.end();
       }
     }
   }
diff --git a/test/parallel/test-http-server-raw-header-lookup.js b/test/parallel/test-http-server-raw-header-lookup.js
new file mode 100644
index 00000000000..620321c0ff3
--- /dev/null
+++ b/test/parallel/test-http-server-raw-header-lookup.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+const net = require('net');
+
+const server = http.createServer({
+  optimizeEmptyRequests: true,
+}, common.mustCall((req, res) => {
+  // Host / Expect lookups must not force materialization of req.headers.
+  // Accessing headers afterwards still works, including unusual casing.
+  assert.strictEqual(req.headers.host, 'example.test');
+  res.writeHead(200, { 'Content-Type': 'text/plain' });
+  res.end('ok');
+}, 3));
+
+server.listen(0, common.mustCall(() => {
+  const port = server.address().port;
+
+  function request(headers) {
+    return new Promise((resolve, reject) => {
+      const socket = net.connect(port, '127.0.0.1', () => {
+        socket.write(headers);
+      });
+      let data = '';
+      socket.setEncoding('utf8');
+      socket.on('data', (chunk) => { data += chunk; });
+      socket.on('end', () => resolve(data));
+      socket.on('error', reject);
+    });
+  }
+
+  (async () => {
+    // Mixed-case Host must satisfy requireHostHeader.
+    const mixedHost = await request(
+      'GET / HTTP/1.1\r\nHOST: example.test\r\nConnection: close\r\n\r\n',
+    );
+    assert.match(mixedHost, /^HTTP\/1\.1 200 /);
+
+    // Unusual Host casing.
+    const oddHost = await request(
+      'GET / HTTP/1.1\r\nhOsT: example.test\r\nConnection: close\r\n\r\n',
+    );
+    assert.match(oddHost, /^HTTP\/1\.1 200 /);
+
+    // Mixed-case Expect: 100-continue still triggers the continue path.
+    const expectContinue = await request(
+      'POST / HTTP/1.1\r\nHost: example.test\r\n' +
+      'EXPECT: 100-continue\r\nContent-Length: 0\r\nConnection: close\r\n\r\n',
+    );
+    assert.match(expectContinue, /^HTTP\/1\.1 100 Continue\r\n/);
+    assert.match(expectContinue, /HTTP\/1\.1 200 /);
+
+    // Missing Host is still rejected.
+    const missingHost = await request(
+      'GET / HTTP/1.1\r\nConnection: close\r\n\r\n',
+    );
+    assert.match(missingHost, /^HTTP\/1\.1 400 /);
+
+    server.close();
+  })().then(common.mustCall());
+}));