Skip to content

Commit 9c06dab

Browse files
authored
http: pass maxHeaderPairs to parser.initialize()
The parser read the `maxHeaderPairs` property from its JS object in C++ once per header section to enforce the header count limit. That lookup is a runtime property load for every parsed request, and costs up to 7% on the parser benchmark for requests with few headers. Pass the limit to `initialize()` instead and keep it in a field. The property is still set, as the JS side uses it to trim the header list. Refs: #64988 Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #66250 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Xuguang Mei <meixuguang@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
1 parent 4bd56b3 commit 9c06dab

7 files changed

Lines changed: 123 additions & 117 deletions

File tree

‎benchmark/http/bench-parser.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,23 @@ function main({ len, n }) {
1616
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
1717
const kOnBody = HTTPParser.kOnBody | 0;
1818
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
19+
const kMaxHeaderPairs = 2000;
1920

2021
function processHeader(header, n) {
2122
const parser = newParser(REQUEST);
2223

2324
bench.start();
2425
for (let i = 0; i < n; i++) {
2526
parser.execute(header, 0, header.length);
26-
parser.initialize(REQUEST, {});
27+
parser.initialize(REQUEST, {}, 0, 0, undefined, kMaxHeaderPairs);
2728
}
2829
bench.end(n);
2930
}
3031

3132
function newParser(type) {
3233
const parser = new HTTPParser();
33-
parser.initialize(type, {});
3434
// Direct parsers bypass cleanParser(); use its production default.
35-
parser.maxHeaderPairs = 2000;
35+
parser.initialize(type, {}, 0, 0, undefined, kMaxHeaderPairs);
3636

3737
parser.headers = [];
3838

‎lib/_http_client.js‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,22 +1102,23 @@ function tickOnSocket(req, socket) {
11021102
const parser = parsers.alloc();
11031103
req.socket = socket;
11041104
const lenientFlags = calculateLenientFlags(req.httpValidation, req.insecureHTTPParser);
1105+
// Propagate headers limit from request object to parser
1106+
if (typeof req.maxHeadersCount === 'number') {
1107+
parser.maxHeaderPairs = req.maxHeadersCount << 1;
1108+
}
11051109
parser.initialize(HTTPParser.RESPONSE,
11061110
new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req),
11071111
req.maxHeaderSize || 0,
1108-
lenientFlags);
1112+
lenientFlags,
1113+
undefined,
1114+
parser.maxHeaderPairs);
11091115
parser.socket = socket;
11101116
parser.outgoing = req;
11111117
req.parser = parser;
11121118

11131119
socket.parser = parser;
11141120
socket._httpMessage = req;
11151121

1116-
// Propagate headers limit from request object to parser
1117-
if (typeof req.maxHeadersCount === 'number') {
1118-
parser.maxHeaderPairs = req.maxHeadersCount << 1;
1119-
}
1120-
11211122
parser.joinDuplicateHeaders = req.joinDuplicateHeaders;
11221123

11231124
parser.onIncoming = parserOnIncomingClient;

‎lib/_http_server.js‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,11 @@ function connectionListenerInternal(server, socket) {
811811

812812
const lenientFlags = calculateLenientFlags(server.httpValidation, server.insecureHTTPParser);
813813

814+
// Propagate headers limit from server instance to parser
815+
if (typeof server.maxHeadersCount === 'number') {
816+
parser.maxHeaderPairs = server.maxHeadersCount << 1;
817+
}
818+
814819
// TODO(addaleax): This doesn't play well with the
815820
// `async_hooks.currentResource()` proposal, see
816821
// https://github.com/nodejs/node/pull/21313
@@ -820,15 +825,11 @@ function connectionListenerInternal(server, socket) {
820825
server.maxHeaderSize || 0,
821826
lenientFlags,
822827
server[kConnections],
828+
parser.maxHeaderPairs,
823829
);
824830
parser.socket = socket;
825831
socket.parser = parser;
826832

827-
// Propagate headers limit from server instance to parser
828-
if (typeof server.maxHeadersCount === 'number') {
829-
parser.maxHeaderPairs = server.maxHeadersCount << 1;
830-
}
831-
832833
const state = {
833834
onData: null,
834835
onEnd: null,

‎src/node_http_parser.cc‎

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "stream_base-inl.h"
3232
#include "v8.h"
3333

34+
#include <algorithm>
3435
#include <cstdlib> // free()
3536
#include <cstring> // strdup(), strchr()
3637

@@ -324,7 +325,6 @@ class Parser : public AsyncWrap, public StreamListener {
324325
allocator_.Reset();
325326
url_.Reset();
326327
status_message_.Reset();
327-
max_header_pairs_ = -1;
328328

329329
if (connectionsList_ != nullptr) {
330330
connectionsList_->PushActive(this);
@@ -465,7 +465,6 @@ class Parser : public AsyncWrap, public StreamListener {
465465
num_fields_ = 0;
466466
num_values_ = 0;
467467
header_pairs_ = 0;
468-
max_header_pairs_ = -1;
469468

470469
// METHOD
471470
if (parser_.type == HTTP_REQUEST) {
@@ -683,6 +682,7 @@ class Parser : public AsyncWrap, public StreamListener {
683682

684683
uint64_t max_http_header_size = 0;
685684
uint32_t lenient_flags = kLenientNone;
685+
size_t max_header_pairs = 0;
686686
ConnectionsList* connectionsList = nullptr;
687687

688688
CHECK(args[0]->IsInt32());
@@ -707,6 +707,12 @@ class Parser : public AsyncWrap, public StreamListener {
707707
ASSIGN_OR_RETURN_UNWRAP(&connectionsList, args[4]);
708708
}
709709

710+
// Non-positive values mean no limit.
711+
if (args.Length() > 5 && !args[5]->IsUndefined()) {
712+
CHECK(args[5]->IsInt32());
713+
max_header_pairs = std::max(args[5].As<Int32>()->Value(), 0);
714+
}
715+
710716
llhttp_type_t type =
711717
static_cast<llhttp_type_t>(args[0].As<Int32>()->Value());
712718

@@ -723,7 +729,7 @@ class Parser : public AsyncWrap, public StreamListener {
723729

724730
parser->set_provider_type(provider);
725731
parser->AsyncReset(args[1].As<Object>());
726-
parser->Init(type, max_http_header_size, lenient_flags);
732+
parser->Init(type, max_http_header_size, lenient_flags, max_header_pairs);
727733

728734
if (connectionsList != nullptr) {
729735
parser->connectionsList_ = connectionsList;
@@ -974,9 +980,10 @@ class Parser : public AsyncWrap, public StreamListener {
974980
have_flushed_ = true;
975981
}
976982

977-
978-
void Init(llhttp_type_t type, uint64_t max_http_header_size,
979-
uint32_t lenient_flags) {
983+
void Init(llhttp_type_t type,
984+
uint64_t max_http_header_size,
985+
uint32_t lenient_flags,
986+
size_t max_header_pairs) {
980987
llhttp_init(&parser_, type, &settings);
981988

982989
if (lenient_flags & kLenientHeaders) {
@@ -1026,10 +1033,9 @@ class Parser : public AsyncWrap, public StreamListener {
10261033
headers_completed_ = false;
10271034
max_http_header_size_ = max_http_header_size;
10281035
header_pairs_ = 0;
1029-
max_header_pairs_ = -1;
1036+
max_header_pairs_ = max_header_pairs;
10301037
}
10311038

1032-
10331039
int TrackHeader(size_t len) {
10341040
header_nread_ += len;
10351041
if (header_nread_ >= max_http_header_size_) {
@@ -1042,22 +1048,6 @@ class Parser : public AsyncWrap, public StreamListener {
10421048
int TrackHeaderPair() {
10431049
header_pairs_ += 2;
10441050

1045-
if (max_header_pairs_ < 0) {
1046-
Local<Value> max_header_pairs_v;
1047-
if (!object()
1048-
->Get(env()->context(),
1049-
FIXED_ONE_BYTE_STRING(env()->isolate(), "maxHeaderPairs"))
1050-
.ToLocal(&max_header_pairs_v)) {
1051-
got_exception_ = true;
1052-
return -1;
1053-
}
1054-
1055-
const double value = max_header_pairs_v->IsNumber()
1056-
? max_header_pairs_v.As<Number>()->Value()
1057-
: 0;
1058-
max_header_pairs_ = value > 0 ? value : 0;
1059-
}
1060-
10611051
if (max_header_pairs_ > 0 && header_pairs_ > max_header_pairs_) {
10621052
llhttp_set_error_reason(&parser_, "HPE_HEADER_OVERFLOW:Header overflow");
10631053
return HPE_USER;
@@ -1100,7 +1090,7 @@ class Parser : public AsyncWrap, public StreamListener {
11001090
const char* current_buffer_data_;
11011091
bool headers_completed_ = false;
11021092
size_t header_pairs_ = 0;
1103-
double max_header_pairs_ = -1;
1093+
size_t max_header_pairs_ = 0;
11041094
bool pending_pause_ = false;
11051095
bool received_data_ = false;
11061096
uint64_t header_nread_ = 0;

‎test/parallel/test-http-parser-max-header-pairs-cache.js‎

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { HTTPParser } = require('_http_common');
6+
7+
const { REQUEST, RESPONSE } = HTTPParser;
8+
const kOnHeaders = HTTPParser.kOnHeaders | 0;
9+
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
10+
const kOnBody = HTTPParser.kOnBody | 0;
11+
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
12+
13+
function createParser(type, ...initArgs) {
14+
const parser = new HTTPParser();
15+
parser.initialize(type, {}, ...initArgs);
16+
parser[kOnHeaders] = () => {};
17+
parser[kOnHeadersComplete] = () => {};
18+
parser[kOnBody] = () => {};
19+
parser[kOnMessageComplete] = () => {};
20+
return parser;
21+
}
22+
23+
function assertOverflow(result) {
24+
assert.ok(result instanceof Error);
25+
assert.strictEqual(result.code, 'HPE_HEADER_OVERFLOW');
26+
}
27+
28+
const twoHeaders = 'X-A: a\r\nX-B: b\r\n';
29+
const threeHeaders = 'X-A: a\r\nX-B: b\r\nX-C: c\r\n';
30+
31+
// The limit passed to initialize() applies to requests and responses.
32+
for (const [type, startLine] of [
33+
[REQUEST, 'GET / HTTP/1.1\r\n'],
34+
[RESPONSE, 'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n'],
35+
]) {
36+
// The response start line carries one header of its own.
37+
const limit = type === REQUEST ? 4 : 6;
38+
39+
const ok = Buffer.from(`${startLine}${twoHeaders}\r\n`);
40+
const parser = createParser(type, 0, 0, undefined, limit);
41+
assert.strictEqual(parser.execute(ok, 0, ok.length), ok.length);
42+
43+
const tooMany = Buffer.from(`${startLine}${threeHeaders}\r\n`);
44+
assertOverflow(createParser(type, 0, 0, undefined, limit)
45+
.execute(tooMany, 0, tooMany.length));
46+
}
47+
48+
// The parser does not read the maxHeaderPairs property.
49+
{
50+
const parser = createParser(REQUEST, 0, 0, undefined, 2);
51+
Object.defineProperty(parser, 'maxHeaderPairs', {
52+
get: common.mustNotCall(),
53+
});
54+
const request = Buffer.from(`GET / HTTP/1.1\r\n${twoHeaders}\r\n`);
55+
assertOverflow(parser.execute(request, 0, request.length));
56+
}
57+
58+
// Main headers, trailers, and the next pipelined message are each counted
59+
// separately against the same limit.
60+
{
61+
const parser = createParser(REQUEST, 0, 0, undefined, 4);
62+
parser[kOnHeadersComplete] = common.mustCall(undefined, 2);
63+
parser[kOnMessageComplete] = common.mustCall(undefined, 2);
64+
65+
const pipelined = Buffer.from(
66+
'POST /first HTTP/1.1\r\n' +
67+
'Transfer-Encoding: chunked\r\n' +
68+
'\r\n' +
69+
'0\r\n' +
70+
twoHeaders +
71+
'\r\n' +
72+
`GET /second HTTP/1.1\r\n${twoHeaders}\r\n`
73+
);
74+
assert.strictEqual(parser.execute(pipelined, 0, pipelined.length), pipelined.length);
75+
}
76+
77+
// Reinitializing the parser replaces the limit.
78+
{
79+
const parser = createParser(REQUEST, 0, 0, undefined, 2);
80+
parser.initialize(REQUEST, {}, 0, 0, undefined, 6);
81+
const request = Buffer.from(`GET / HTTP/1.1\r\n${threeHeaders}\r\n`);
82+
assert.strictEqual(parser.execute(request, 0, request.length), request.length);
83+
}
84+
85+
// An omitted or non-positive limit means unlimited.
86+
for (const initArgs of [[], [0, 0, undefined, 0], [0, 0, undefined, -1]]) {
87+
const parser = createParser(REQUEST, ...initArgs);
88+
const request = Buffer.from(`GET / HTTP/1.1\r\n${threeHeaders}\r\n`);
89+
assert.strictEqual(parser.execute(request, 0, request.length), request.length);
90+
}

‎typings/internalBinding/http_parser.d.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ declare namespace InternalHttpParserBinding {
4646
resource: object,
4747
maxHeaderSize?: number,
4848
lenient?: number,
49-
headersTimeout?: number,
49+
connectionsList?: object,
50+
maxHeaderPairs?: number,
5051
): void;
5152
pause(): void;
5253
resume(): void;

0 commit comments

Comments
 (0)