Files
node/test/parallel/test-http-parser-cached-callbacks.js
Matteo Collina 0519f500b4 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>
PR-URL: https://github.com/nodejs/node/pull/66152
Reviewed-By: Robert Nagy <ronagy@icloud.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
2026-09-27 08:10:01 +00:00

62 lines
1.9 KiB
JavaScript

// Flags: --expose-gc
'use strict';
const common = require('../common');
const assert = require('assert');
const { HTTPParser } = require('_http_common');
const { gcUntil } = require('../common/gc');
const request = Buffer.from('POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody');
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
const kOnBody = HTTPParser.kOnBody | 0;
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
const parser = new HTTPParser();
parser.tag = 'parser';
parser.initialize(HTTPParser.REQUEST, {});
let calls = 0;
for (const callback of [kOnHeadersComplete, kOnBody, kOnMessageComplete]) {
parser[callback] = common.mustCall(() => {
assert.strictEqual(parser.tag, 'parser');
calls++;
}, 2);
}
parser.execute(request);
parser.execute(request);
assert.strictEqual(calls, 6);
// Reinitializing must replace each cached callback.
parser.initialize(HTTPParser.REQUEST, {});
for (const callback of [kOnHeadersComplete, kOnBody, kOnMessageComplete]) {
parser[callback] = common.mustCall(() => {
assert.strictEqual(parser.tag, 'parser');
calls += 2;
});
}
parser.execute(request);
assert.strictEqual(calls, 12);
// Freeing a parser must release a cached callback even if the parser itself
// remains reachable and its JS callback property has been replaced.
function freedCallback() {
const parser = new HTTPParser();
parser.initialize(HTTPParser.REQUEST, {});
let callback = () => parser;
parser[kOnHeadersComplete] = callback;
parser.execute(request);
const ref = new WeakRef(callback);
parser[kOnHeadersComplete] = null;
callback = null;
parser.free();
return { parser, ref };
}
const { parser: freedParser, ref } = freedCallback();
gcUntil('freed HTTPParser callback', () => ref.deref() === undefined)
.then(common.mustCall(() => {
// Keep the parser alive while its callback is collected.
assert.ok(freedParser);
}));