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());
+}));