Skip to content

Commit 4bd56b3

Browse files
authored
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: #65802 Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: a closed-source coding agent PR-URL: #66120 Refs: #65332 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com>
1 parent 823d229 commit 4bd56b3

4 files changed

Lines changed: 165 additions & 30 deletions

File tree

‎lib/_http_incoming.js‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,15 @@ function IncomingMessage(socket) {
6363
return deprecateInstantiation(IncomingMessage, 'DEP0195', socket);
6464
}
6565

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

6868
if (socket) {
69-
streamOptions = {
70-
highWaterMark: socket.readableHighWaterMark,
71-
};
69+
const hwm = socket.readableHighWaterMark;
70+
if (this._readableState.highWaterMark !== hwm) {
71+
this._readableState.highWaterMark = hwm;
72+
}
7273
}
7374

74-
Readable.call(this, streamOptions);
75-
7675
this._readableState.readingMore = true;
7776

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

532+
// Case-insensitive ASCII compare against an already-lowercased name.
533+
// Avoids allocating a lowercased copy of every header name.
534+
function asciiEqualIgnoreCase(a, lower) {
535+
const len = lower.length;
536+
if (a.length !== len)
537+
return false;
538+
if (a === lower)
539+
return true;
540+
for (let i = 0; i < len; i++) {
541+
let c = a.charCodeAt(i);
542+
if (c >= 65 && c <= 90)
543+
c += 32;
544+
if (c !== lower.charCodeAt(i))
545+
return false;
546+
}
547+
return true;
548+
}
549+
550+
function getRawHeader(msg, lowerName, joinDuplicates) {
551+
const rawHeaders = msg.rawHeaders;
552+
const count = msg[kHeadersCount];
553+
let result;
554+
for (let i = 0; i < count; i += 2) {
555+
if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
556+
continue;
557+
const value = rawHeaders[i + 1];
558+
if (result === undefined) {
559+
result = value;
560+
if (!joinDuplicates)
561+
return result;
562+
} else {
563+
result += ', ' + value;
564+
}
565+
}
566+
return result;
567+
}
568+
533569
function onError(self, error, cb) {
534570
// This is to keep backward compatible behavior.
535571
// An error is emitted only if there are listeners attached to the event.
@@ -544,6 +580,8 @@ module.exports = {
544580
IncomingMessage,
545581
kDetachAbortSignal,
546582
kHeadersCount,
583+
asciiEqualIgnoreCase,
584+
getRawHeader,
547585
readStart,
548586
readStop,
549587
};

‎lib/_http_outgoing.js‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,10 +659,38 @@ function storeHeader(self, state, key, value, validate, lenient) {
659659
matchHeader(self, state, key, value);
660660
}
661661

662+
function lowerOutgoingHeaderName(field) {
663+
switch (field) {
664+
case 'Connection':
665+
case 'connection':
666+
return 'connection';
667+
case 'Content-Length':
668+
case 'content-length':
669+
return 'content-length';
670+
case 'Transfer-Encoding':
671+
case 'transfer-encoding':
672+
return 'transfer-encoding';
673+
case 'Date':
674+
case 'date':
675+
return 'date';
676+
case 'Expect':
677+
case 'expect':
678+
return 'expect';
679+
case 'Trailer':
680+
case 'trailer':
681+
return 'trailer';
682+
case 'Keep-Alive':
683+
case 'keep-alive':
684+
return 'keep-alive';
685+
default:
686+
return field.toLowerCase();
687+
}
688+
}
689+
662690
function matchHeader(self, state, field, value) {
663691
if (field.length < 4 || field.length > 17)
664692
return;
665-
field = field.toLowerCase();
693+
field = lowerOutgoingHeaderName(field);
666694
switch (field) {
667695
case 'connection':
668696
state.connection = true;

‎lib/_http_server.js‎

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ const {
7474
IncomingMessage,
7575
kDetachAbortSignal,
7676
kHeadersCount,
77+
asciiEqualIgnoreCase,
78+
getRawHeader,
7779
} = require('_http_incoming');
7880
const {
7981
ConnResetException,
@@ -225,7 +227,8 @@ function ServerResponse(req, options) {
225227
this._expect_continue = false;
226228

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

@@ -1289,10 +1292,8 @@ function hasRequestHeader(req, length, lowerName) {
12891292
const count = req[kHeadersCount];
12901293
for (let i = 0; i < count; i += 2) {
12911294
const key = rawHeaders[i];
1292-
if (key.length === length &&
1293-
(key === lowerName || key.toLowerCase() === lowerName)) {
1295+
if (key.length === length && asciiEqualIgnoreCase(key, lowerName))
12941296
return true;
1295-
}
12961297
}
12971298
return false;
12981299
}
@@ -1301,12 +1302,14 @@ function hasBodyHeaders(req) {
13011302
const rawHeaders = req.rawHeaders;
13021303
const count = req[kHeadersCount];
13031304
for (let i = 0; i < count; i += 2) {
1304-
const length = rawHeaders[i].length;
1305-
if (length === 14 || length === 17) {
1306-
const key = rawHeaders[i].toLowerCase();
1307-
if (key === 'content-length' || key === 'transfer-encoding') {
1305+
const key = rawHeaders[i];
1306+
const length = key.length;
1307+
if (length === 14) {
1308+
if (asciiEqualIgnoreCase(key, 'content-length'))
1309+
return true;
1310+
} else if (length === 17) {
1311+
if (asciiEqualIgnoreCase(key, 'transfer-encoding'))
13081312
return true;
1309-
}
13101313
}
13111314
}
13121315
return false;
@@ -1441,22 +1444,25 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
14411444
server.emit('dropRequest', req, socket);
14421445
res.writeHead(503);
14431446
res.end();
1444-
} else if (hasRequestHeader(req, 6, 'expect')) {
1445-
handled = true;
1446-
1447-
if (continueExpression.test(req.headers.expect)) {
1448-
res._expect_continue = true;
1449-
if (server.listenerCount('checkContinue') > 0) {
1450-
server.emit('checkContinue', req, res);
1447+
} else {
1448+
const expect = getRawHeader(req, 'expect', true);
1449+
if (expect !== undefined) {
1450+
handled = true;
1451+
1452+
if (continueExpression.test(expect)) {
1453+
res._expect_continue = true;
1454+
if (server.listenerCount('checkContinue') > 0) {
1455+
server.emit('checkContinue', req, res);
1456+
} else {
1457+
res.writeContinue();
1458+
server.emit('request', req, res);
1459+
}
1460+
} else if (server.listenerCount('checkExpectation') > 0) {
1461+
server.emit('checkExpectation', req, res);
14511462
} else {
1452-
res.writeContinue();
1453-
server.emit('request', req, res);
1463+
res.writeHead(417);
1464+
res.end();
14541465
}
1455-
} else if (server.listenerCount('checkExpectation') > 0) {
1456-
server.emit('checkExpectation', req, res);
1457-
} else {
1458-
res.writeHead(417);
1459-
res.end();
14601466
}
14611467
}
14621468
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
const net = require('net');
7+
8+
const server = http.createServer({
9+
optimizeEmptyRequests: true,
10+
}, common.mustCall((req, res) => {
11+
// Host / Expect lookups must not force materialization of req.headers.
12+
// Accessing headers afterwards still works, including unusual casing.
13+
assert.strictEqual(req.headers.host, 'example.test');
14+
res.writeHead(200, { 'Content-Type': 'text/plain' });
15+
res.end('ok');
16+
}, 3));
17+
18+
server.listen(0, common.mustCall(() => {
19+
const port = server.address().port;
20+
21+
function request(headers) {
22+
return new Promise((resolve, reject) => {
23+
const socket = net.connect(port, '127.0.0.1', () => {
24+
socket.write(headers);
25+
});
26+
let data = '';
27+
socket.setEncoding('utf8');
28+
socket.on('data', (chunk) => { data += chunk; });
29+
socket.on('end', () => resolve(data));
30+
socket.on('error', reject);
31+
});
32+
}
33+
34+
(async () => {
35+
// Mixed-case Host must satisfy requireHostHeader.
36+
const mixedHost = await request(
37+
'GET / HTTP/1.1\r\nHOST: example.test\r\nConnection: close\r\n\r\n',
38+
);
39+
assert.match(mixedHost, /^HTTP\/1\.1 200 /);
40+
41+
// Unusual Host casing.
42+
const oddHost = await request(
43+
'GET / HTTP/1.1\r\nhOsT: example.test\r\nConnection: close\r\n\r\n',
44+
);
45+
assert.match(oddHost, /^HTTP\/1\.1 200 /);
46+
47+
// Mixed-case Expect: 100-continue still triggers the continue path.
48+
const expectContinue = await request(
49+
'POST / HTTP/1.1\r\nHost: example.test\r\n' +
50+
'EXPECT: 100-continue\r\nContent-Length: 0\r\nConnection: close\r\n\r\n',
51+
);
52+
assert.match(expectContinue, /^HTTP\/1\.1 100 Continue\r\n/);
53+
assert.match(expectContinue, /HTTP\/1\.1 200 /);
54+
55+
// Missing Host is still rejected.
56+
const missingHost = await request(
57+
'GET / HTTP/1.1\r\nConnection: close\r\n\r\n',
58+
);
59+
assert.match(missingHost, /^HTTP\/1\.1 400 /);
60+
61+
server.close();
62+
})().then(common.mustCall());
63+
}));

0 commit comments

Comments
 (0)