Skip to content

Commit 2504fd1

Browse files
committed
http: cache parser callbacks
Cache the per-message callback lookups on the parser's JS object instead of retaining callback functions in strong v8::Global handles. A callback that captures its parser can otherwise keep the parser alive. Clear the cache when a parser is initialized or freed so reused parsers can load replacement callbacks and idle parsers do not retain them. Header field names remain non-internalized because they are supplied by clients. Assisted-by: pi Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent c0681e5 commit 2504fd1

2 files changed

Lines changed: 98 additions & 7 deletions

File tree

‎src/node_http_parser.cc‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,13 @@ class Parser : public AsyncWrap, public StreamListener {
312312
current_buffer_data_(nullptr),
313313
binding_data_(binding_data) {}
314314

315+
enum InternalFields {
316+
kOnHeadersCompleteCallback = AsyncWrap::kInternalFieldCount,
317+
kOnBodyCallback,
318+
kOnMessageCompleteCallback,
319+
kInternalFieldCount
320+
};
321+
315322
SET_NO_MEMORY_INFO()
316323
SET_MEMORY_INFO_NAME(Parser)
317324
SET_SELF_SIZE(Parser)
@@ -441,9 +448,8 @@ class Parser : public AsyncWrap, public StreamListener {
441448
};
442449

443450
Local<Value> argv[A_MAX];
444-
Local<Object> obj = object();
445-
Local<Value> cb = obj->Get(env()->context(),
446-
kOnHeadersComplete).ToLocalChecked();
451+
Local<Value> cb =
452+
CachedCallback(kOnHeadersComplete, kOnHeadersCompleteCallback);
447453

448454
if (!cb->IsFunction())
449455
return 0;
@@ -520,7 +526,7 @@ class Parser : public AsyncWrap, public StreamListener {
520526
Environment* env = this->env();
521527
HandleScope handle_scope(env->isolate());
522528

523-
Local<Value> cb = object()->Get(env->context(), kOnBody).ToLocalChecked();
529+
Local<Value> cb = CachedCallback(kOnBody, kOnBodyCallback);
524530

525531
if (!cb->IsFunction())
526532
return 0;
@@ -553,9 +559,8 @@ class Parser : public AsyncWrap, public StreamListener {
553559

554560
header_pairs_ = 0;
555561

556-
Local<Object> obj = object();
557-
Local<Value> cb = obj->Get(env()->context(),
558-
kOnMessageComplete).ToLocalChecked();
562+
Local<Value> cb =
563+
CachedCallback(kOnMessageComplete, kOnMessageCompleteCallback);
559564

560565
if (!cb->IsFunction())
561566
return 0;
@@ -624,6 +629,7 @@ class Parser : public AsyncWrap, public StreamListener {
624629
// it needs to be triggered manually.
625630
parser->EmitTraceEventDestroy();
626631
parser->EmitDestroy();
632+
parser->ClearCachedCallbacks();
627633
}
628634

629635
// TODO(@anonrig): Add V8 Fast API
@@ -946,6 +952,9 @@ class Parser : public AsyncWrap, public StreamListener {
946952
Local<Value> headers_v[kMaxHeaderFieldsCount * 2];
947953

948954
for (size_t i = 0; i < num_values_; ++i) {
955+
// Field names are not internalized: header names are attacker
956+
// controlled, so a flood of unique names would grow V8's string table
957+
// and pay the interning cost on every request with no dedup benefit.
949958
headers_v[i * 2] = fields_[i].ToString(env());
950959
headers_v[i * 2 + 1] = values_[i].ToTrimmedString(env());
951960
}
@@ -980,12 +989,33 @@ class Parser : public AsyncWrap, public StreamListener {
980989
have_flushed_ = true;
981990
}
982991

992+
void ClearCachedCallbacks() {
993+
Local<Value> undefined = Undefined(env()->isolate());
994+
object()->SetInternalField(kOnHeadersCompleteCallback, undefined);
995+
object()->SetInternalField(kOnBodyCallback, undefined);
996+
object()->SetInternalField(kOnMessageCompleteCallback, undefined);
997+
}
998+
999+
// Keep cached callbacks on the JS object so they do not keep the parser
1000+
// alive when a callback closes over it.
1001+
Local<Value> CachedCallback(uint32_t index, int field) {
1002+
Local<Object> obj = object();
1003+
Local<Value> cb = obj->GetInternalField(field).As<Value>();
1004+
if (cb->IsFunction()) return cb;
1005+
1006+
cb = obj->Get(env()->context(), index).ToLocalChecked();
1007+
if (cb->IsFunction()) obj->SetInternalField(field, cb);
1008+
return cb;
1009+
}
1010+
9831011
void Init(llhttp_type_t type,
9841012
uint64_t max_http_header_size,
9851013
uint32_t lenient_flags,
9861014
size_t max_header_pairs) {
9871015
llhttp_init(&parser_, type, &settings);
9881016

1017+
ClearCachedCallbacks();
1018+
9891019
if (lenient_flags & kLenientHeaders) {
9901020
llhttp_set_lenient_headers(&parser_, 1);
9911021
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const { HTTPParser } = require('_http_common');
7+
const { gcUntil } = require('../common/gc');
8+
9+
const request = Buffer.from('POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody');
10+
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
11+
const kOnBody = HTTPParser.kOnBody | 0;
12+
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
13+
14+
const parser = new HTTPParser();
15+
parser.tag = 'parser';
16+
parser.initialize(HTTPParser.REQUEST, {});
17+
18+
let calls = 0;
19+
for (const callback of [kOnHeadersComplete, kOnBody, kOnMessageComplete]) {
20+
parser[callback] = common.mustCall(() => {
21+
assert.strictEqual(parser.tag, 'parser');
22+
calls++;
23+
}, 2);
24+
}
25+
26+
parser.execute(request);
27+
parser.execute(request);
28+
assert.strictEqual(calls, 6);
29+
30+
// Reinitializing must replace each cached callback.
31+
parser.initialize(HTTPParser.REQUEST, {});
32+
for (const callback of [kOnHeadersComplete, kOnBody, kOnMessageComplete]) {
33+
parser[callback] = common.mustCall(() => {
34+
assert.strictEqual(parser.tag, 'parser');
35+
calls += 2;
36+
});
37+
}
38+
parser.execute(request);
39+
assert.strictEqual(calls, 12);
40+
41+
// Freeing a parser must release a cached callback even if the parser itself
42+
// remains reachable and its JS callback property has been replaced.
43+
function freedCallback() {
44+
const parser = new HTTPParser();
45+
parser.initialize(HTTPParser.REQUEST, {});
46+
let callback = () => parser;
47+
parser[kOnHeadersComplete] = callback;
48+
parser.execute(request);
49+
const ref = new WeakRef(callback);
50+
parser[kOnHeadersComplete] = null;
51+
callback = null;
52+
parser.free();
53+
return { parser, ref };
54+
}
55+
56+
const { parser: freedParser, ref } = freedCallback();
57+
gcUntil('freed HTTPParser callback', () => ref.deref() === undefined)
58+
.then(common.mustCall(() => {
59+
// Keep the parser alive while its callback is collected.
60+
assert.ok(freedParser);
61+
}));

0 commit comments

Comments
 (0)