Skip to content

Commit 0697b10

Browse files
anonrigaduh95
authored andcommitted
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 14c5e7c commit 0697b10

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
@@ -56,16 +56,15 @@ function readStop(socket) {
5656

5757
/* Abstract base class for ServerRequest and ClientResponse. */
5858
function IncomingMessage(socket) {
59-
let streamOptions;
59+
Readable.call(this);
6060

6161
if (socket) {
62-
streamOptions = {
63-
highWaterMark: socket.readableHighWaterMark,
64-
};
62+
const hwm = socket.readableHighWaterMark;
63+
if (this._readableState.highWaterMark !== hwm) {
64+
this._readableState.highWaterMark = hwm;
65+
}
6566
}
6667

67-
Readable.call(this, streamOptions);
68-
6968
this._readableState.readingMore = true;
7069

7170
this.socket = socket;
@@ -579,6 +578,43 @@ IncomingMessage.prototype._dump = function _dump() {
579578
}
580579
};
581580

581+
// Case-insensitive ASCII compare against an already-lowercased name.
582+
// Avoids allocating a lowercased copy of every header name.
583+
function asciiEqualIgnoreCase(a, lower) {
584+
const len = lower.length;
585+
if (a.length !== len)
586+
return false;
587+
if (a === lower)
588+
return true;
589+
for (let i = 0; i < len; i++) {
590+
let c = a.charCodeAt(i);
591+
if (c >= 65 && c <= 90)
592+
c += 32;
593+
if (c !== lower.charCodeAt(i))
594+
return false;
595+
}
596+
return true;
597+
}
598+
599+
function getRawHeader(msg, lowerName, joinDuplicates) {
600+
const rawHeaders = msg.rawHeaders;
601+
const count = msg[kHeadersCount];
602+
let result;
603+
for (let i = 0; i < count; i += 2) {
604+
if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
605+
continue;
606+
const value = rawHeaders[i + 1];
607+
if (result === undefined) {
608+
result = value;
609+
if (!joinDuplicates)
610+
return result;
611+
} else {
612+
result += ', ' + value;
613+
}
614+
}
615+
return result;
616+
}
617+
582618
function onError(self, error, cb) {
583619
// This is to keep backward compatible behavior.
584620
// An error is emitted only if there are listeners attached to the event.
@@ -593,6 +629,8 @@ module.exports = {
593629
IncomingMessage,
594630
kDetachAbortSignal,
595631
kHeadersCount,
632+
asciiEqualIgnoreCase,
633+
getRawHeader,
596634
readStart,
597635
readStop,
598636
};

‎lib/_http_outgoing.js‎

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

655+
function lowerOutgoingHeaderName(field) {
656+
switch (field) {
657+
case 'Connection':
658+
case 'connection':
659+
return 'connection';
660+
case 'Content-Length':
661+
case 'content-length':
662+
return 'content-length';
663+
case 'Transfer-Encoding':
664+
case 'transfer-encoding':
665+
return 'transfer-encoding';
666+
case 'Date':
667+
case 'date':
668+
return 'date';
669+
case 'Expect':
670+
case 'expect':
671+
return 'expect';
672+
case 'Trailer':
673+
case 'trailer':
674+
return 'trailer';
675+
case 'Keep-Alive':
676+
case 'keep-alive':
677+
return 'keep-alive';
678+
default:
679+
return field.toLowerCase();
680+
}
681+
}
682+
655683
function matchHeader(self, state, field, value) {
656684
if (field.length < 4 || field.length > 17)
657685
return;
658-
field = field.toLowerCase();
686+
field = lowerOutgoingHeaderName(field);
659687
switch (field) {
660688
case 'connection':
661689
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,
@@ -220,7 +222,8 @@ function ServerResponse(req, options) {
220222
this._expect_continue = false;
221223

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

@@ -1282,10 +1285,8 @@ function hasRequestHeader(req, length, lowerName) {
12821285
const count = req[kHeadersCount];
12831286
for (let i = 0; i < count; i += 2) {
12841287
const key = rawHeaders[i];
1285-
if (key.length === length &&
1286-
(key === lowerName || key.toLowerCase() === lowerName)) {
1288+
if (key.length === length && asciiEqualIgnoreCase(key, lowerName))
12871289
return true;
1288-
}
12891290
}
12901291
return false;
12911292
}
@@ -1294,12 +1295,14 @@ function hasBodyHeaders(req) {
12941295
const rawHeaders = req.rawHeaders;
12951296
const count = req[kHeadersCount];
12961297
for (let i = 0; i < count; i += 2) {
1297-
const length = rawHeaders[i].length;
1298-
if (length === 14 || length === 17) {
1299-
const key = rawHeaders[i].toLowerCase();
1300-
if (key === 'content-length' || key === 'transfer-encoding') {
1298+
const key = rawHeaders[i];
1299+
const length = key.length;
1300+
if (length === 14) {
1301+
if (asciiEqualIgnoreCase(key, 'content-length'))
1302+
return true;
1303+
} else if (length === 17) {
1304+
if (asciiEqualIgnoreCase(key, 'transfer-encoding'))
13011305
return true;
1302-
}
13031306
}
13041307
}
13051308
return false;
@@ -1434,22 +1437,25 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
14341437
server.emit('dropRequest', req, socket);
14351438
res.writeHead(503);
14361439
res.end();
1437-
} else if (hasRequestHeader(req, 6, 'expect')) {
1438-
handled = true;
1439-
1440-
if (continueExpression.test(req.headers.expect)) {
1441-
res._expect_continue = true;
1442-
if (server.listenerCount('checkContinue') > 0) {
1443-
server.emit('checkContinue', req, res);
1440+
} else {
1441+
const expect = getRawHeader(req, 'expect', true);
1442+
if (expect !== undefined) {
1443+
handled = true;
1444+
1445+
if (continueExpression.test(expect)) {
1446+
res._expect_continue = true;
1447+
if (server.listenerCount('checkContinue') > 0) {
1448+
server.emit('checkContinue', req, res);
1449+
} else {
1450+
res.writeContinue();
1451+
server.emit('request', req, res);
1452+
}
1453+
} else if (server.listenerCount('checkExpectation') > 0) {
1454+
server.emit('checkExpectation', req, res);
14441455
} else {
1445-
res.writeContinue();
1446-
server.emit('request', req, res);
1456+
res.writeHead(417);
1457+
res.end();
14471458
}
1448-
} else if (server.listenerCount('checkExpectation') > 0) {
1449-
server.emit('checkExpectation', req, res);
1450-
} else {
1451-
res.writeHead(417);
1452-
res.end();
14531459
}
14541460
}
14551461
}
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)