From d60af788b14cae02d492196f853bd1f0aa13f5b5 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Fri, 24 Apr 2026 01:49:56 +0800 Subject: [PATCH 01/18] inspector,http: support builtin http request bodies Signed-off-by: GrinZero <774933704@qq.com> --- lib/_http_client.js | 2 + lib/_http_common.js | 12 ++ lib/_http_outgoing.js | 26 ++- lib/internal/http.js | 1 + lib/internal/inspector/network_http.js | 75 +++++++-- .../parallel/test-diagnostics-channel-http.js | 78 +++++++-- test/parallel/test-inspector-network-http.js | 156 +++++++++++++++++- 7 files changed, 321 insertions(+), 29 deletions(-) diff --git a/lib/_http_client.js b/lib/_http_client.js index b7f0aa759b0643..410348ae46656c 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -60,6 +60,7 @@ const { Buffer } = require('buffer'); const { defaultTriggerAsyncIdScope } = require('internal/async_hooks'); const { URL, urlToHttpOptions, isURL } = require('internal/url'); const { + kIsClientRequest, kOutHeaders, kNeedDrain, isTraceHTTPEnabled, @@ -192,6 +193,7 @@ function rewriteForProxiedHttp(req, reqOptions) { function ClientRequest(input, options, cb) { OutgoingMessage.call(this); + this[kIsClientRequest] = true; if (typeof input === 'string') { const urlStr = input; diff --git a/lib/_http_common.js b/lib/_http_common.js index 3c389ba054decc..39a8e56aab9d50 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -27,6 +27,7 @@ const { Uint8Array, } = primordials; const { setImmediate } = require('timers'); +const dc = require('diagnostics_channel'); const { methods, allMethods, HTTPParser } = internalBinding('http_parser'); const { getOptionValue } = require('internal/options'); @@ -50,6 +51,9 @@ const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; const kOnExecute = HTTPParser.kOnExecute | 0; const kOnTimeout = HTTPParser.kOnTimeout | 0; +const onClientResponseBodyChunkReceivedChannel = + dc.channel('http.client.response.bodyChunkReceived'); + const MAX_HEADER_PAIRS = 2000; // Only called in the slow case where slow means @@ -120,6 +124,7 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, // client only incoming.statusCode = statusCode; incoming.statusMessage = statusMessage; + incoming.req = socket?._httpMessage; } return parser.onIncoming(incoming, shouldKeepAlive); @@ -134,6 +139,13 @@ function parserOnBody(b) { // Pretend this was the result of a stream._read call. if (!stream._dumped) { + if (stream.req && onClientResponseBodyChunkReceivedChannel.hasSubscribers) { + onClientResponseBodyChunkReceivedChannel.publish({ + request: stream.req, + response: stream, + chunk: b, + }); + } const ret = stream.push(b); if (!ret) readStop(this.socket); diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 5a83849086294f..91e51229624f67 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -36,9 +36,10 @@ const { const { getDefaultHighWaterMark } = require('internal/streams/state'); const assert = require('internal/assert'); +const dc = require('diagnostics_channel'); const EE = require('events'); const Stream = require('stream'); -const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http'); +const { kIsClientRequest, kOutHeaders, utcDate, kNeedDrain } = require('internal/http'); const { Buffer } = require('buffer'); const { _checkIsHttpToken: checkIsHttpToken, @@ -86,6 +87,12 @@ const kBytesWritten = Symbol('kBytesWritten'); const kErrored = Symbol('errored'); const kHighWaterMark = Symbol('kHighWaterMark'); const kRejectNonStandardBodyWrites = Symbol('kRejectNonStandardBodyWrites'); +const kClientRequestBodyChunksWritten = Symbol('kClientRequestBodyChunksWritten'); + +const onClientRequestBodyChunkSentChannel = + dc.channel('http.client.request.bodyChunkSent'); +const onClientRequestBodySentChannel = + dc.channel('http.client.request.bodySent'); const nop = () => {}; @@ -950,6 +957,17 @@ function write_(msg, chunk, encoding, callback, fromEnd) { } } + if (msg[kIsClientRequest]) { + msg[kClientRequestBodyChunksWritten] = true; + if (onClientRequestBodyChunkSentChannel.hasSubscribers) { + onClientRequestBodyChunkSentChannel.publish({ + request: msg, + chunk, + encoding, + }); + } + } + if (!fromEnd && msg.socket && !msg.socket.writableCorked) { msg.socket.cork(); process.nextTick(connectionCorkNT, msg.socket); @@ -1103,6 +1121,12 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { this.finished = true; + if (this[kIsClientRequest] && + this[kClientRequestBodyChunksWritten] && + onClientRequestBodySentChannel.hasSubscribers) { + onClientRequestBodySentChannel.publish({ request: this }); + } + // There is the first message on the outgoing queue, and we've sent // everything to the socket. debug('outgoing message end.'); diff --git a/lib/internal/http.js b/lib/internal/http.js index 54f1121eb712c0..ee3491278bfe16 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -262,6 +262,7 @@ function getGlobalAgent(proxyEnv, Agent) { } module.exports = { + kIsClientRequest: Symbol('kIsClientRequest'), kOutHeaders: Symbol('kOutHeaders'), kNeedDrain: Symbol('kNeedDrain'), kProxyConfig: Symbol('kProxyConfig'), diff --git a/lib/internal/inspector/network_http.js b/lib/internal/inspector/network_http.js index 46bdd827c094a1..52614540ad16d5 100644 --- a/lib/internal/inspector/network_http.js +++ b/lib/internal/inspector/network_http.js @@ -18,7 +18,7 @@ const { sniffMimeType, } = require('internal/inspector/network'); const { Network } = require('inspector'); -const EventEmitter = require('events'); +const { Buffer } = require('buffer'); const kRequestUrl = Symbol('kRequestUrl'); @@ -88,6 +88,7 @@ function onClientRequestCreated({ request }) { url, method: request.method, headers, + hasPostData: !request.writableEnded, }, }); } @@ -109,6 +110,64 @@ function onClientRequestError({ request, error }) { }); } +/** + * When a chunk of the request body is being sent, cache it until + * `getRequestPostData` request. + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getRequestPostData + * @param {{ request: import('http').ClientRequest, chunk: Uint8Array | string, encoding?: string }} event + */ +function onClientRequestBodyChunkSent({ request, chunk, encoding }) { + if (typeof request[kInspectorRequestId] !== 'string') { + return; + } + + const buffer = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : Buffer.from(chunk); + Network.dataSent({ + requestId: request[kInspectorRequestId], + timestamp: getMonotonicTime(), + dataLength: buffer.byteLength, + data: buffer, + }); +} + +/** + * Mark a request body as fully sent. + * @param {{ request: import('http').ClientRequest }} event + */ +function onClientRequestBodySent({ request }) { + if (typeof request[kInspectorRequestId] !== 'string') { + return; + } + + Network.dataSent({ + requestId: request[kInspectorRequestId], + timestamp: getMonotonicTime(), + dataLength: 0, + data: Buffer.alloc(0), + finished: true, + }); +} + +/** + * When a chunk of the response body is received, cache the raw bytes until + * `getResponseBody` request. + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody + * @param {{ request: import('http').ClientRequest, chunk: Uint8Array }} event + */ +function onClientResponseBodyChunkReceived({ request, chunk }) { + if (typeof request[kInspectorRequestId] !== 'string') { + return; + } + + Network.dataReceived({ + requestId: request[kInspectorRequestId], + timestamp: getMonotonicTime(), + dataLength: chunk.byteLength, + encodedDataLength: chunk.byteLength, + data: chunk, + }); +} + /** * When response headers are received, emit Network.responseReceived event. * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived @@ -135,17 +194,6 @@ function onClientResponseFinish({ request, response }) { }, }); - // Unlike response.on('data', ...), this does not put the stream into flowing mode. - EventEmitter.prototype.on.call(response, 'data', (chunk) => { - Network.dataReceived({ - requestId: request[kInspectorRequestId], - timestamp: getMonotonicTime(), - dataLength: chunk.byteLength, - encodedDataLength: chunk.byteLength, - data: chunk, - }); - }); - // Wait until the response body is consumed by user code. response.once('end', () => { Network.loadingFinished({ @@ -157,6 +205,9 @@ function onClientResponseFinish({ request, response }) { module.exports = registerDiagnosticChannels([ ['http.client.request.created', onClientRequestCreated], + ['http.client.request.bodyChunkSent', onClientRequestBodyChunkSent], + ['http.client.request.bodySent', onClientRequestBodySent], ['http.client.request.error', onClientRequestError], + ['http.client.response.bodyChunkReceived', onClientResponseBodyChunkReceived], ['http.client.response.finish', onClientResponseFinish], ]); diff --git a/test/parallel/test-diagnostics-channel-http.js b/test/parallel/test-diagnostics-channel-http.js index fd371a5d259f0b..ed89f876d74abd 100644 --- a/test/parallel/test-diagnostics-channel-http.js +++ b/test/parallel/test-diagnostics-channel-http.js @@ -14,6 +14,16 @@ const isError = (error) => error instanceof Error; dc.subscribe('http.client.request.start', common.mustCall(({ request }) => { assert.strictEqual(isOutgoingMessage(request), true); +}, 4)); + +dc.subscribe('http.client.request.bodyChunkSent', common.mustCall(({ request, chunk, encoding }) => { + assert.strictEqual(isOutgoingMessage(request), true); + assert.ok(typeof chunk === 'string' || chunk instanceof Uint8Array); + assert.strictEqual(typeof encoding === 'string' || encoding == null, true); +}, 3)); + +dc.subscribe('http.client.request.bodySent', common.mustCall(({ request }) => { + assert.strictEqual(isOutgoingMessage(request), true); }, 2)); dc.subscribe('http.client.request.error', common.mustCall(({ request, error }) => { @@ -21,13 +31,23 @@ dc.subscribe('http.client.request.error', common.mustCall(({ request, error }) = assert.strictEqual(isError(error), true); })); +dc.subscribe('http.client.response.bodyChunkReceived', common.mustCall(({ + request, + response, + chunk, +}) => { + assert.strictEqual(isOutgoingMessage(request), true); + assert.strictEqual(isIncomingMessage(response), true); + assert.ok(chunk instanceof Uint8Array); +}, 3)); + dc.subscribe('http.client.response.finish', common.mustCall(({ request, response }) => { assert.strictEqual(isOutgoingMessage(request), true); assert.strictEqual(isIncomingMessage(response), true); -})); +}, 3)); dc.subscribe('http.server.request.start', common.mustCall(({ request, @@ -39,7 +59,7 @@ dc.subscribe('http.server.request.start', common.mustCall(({ assert.strictEqual(isOutgoingMessage(response), true); assert.strictEqual(isNetSocket(socket), true); assert.strictEqual(isHTTPServer(server), true); -})); +}, 3)); dc.subscribe('http.server.response.finish', common.mustCall(({ request, @@ -51,7 +71,7 @@ dc.subscribe('http.server.response.finish', common.mustCall(({ assert.strictEqual(isOutgoingMessage(response), true); assert.strictEqual(isNetSocket(socket), true); assert.strictEqual(isHTTPServer(server), true); -})); +}, 3)); dc.subscribe('http.server.response.created', common.mustCall(({ request, @@ -59,16 +79,29 @@ dc.subscribe('http.server.response.created', common.mustCall(({ }) => { assert.strictEqual(isIncomingMessage(request), true); assert.strictEqual(isOutgoingMessage(response), true); -})); +}, 3)); dc.subscribe('http.client.request.created', common.mustCall(({ request }) => { assert.strictEqual(isOutgoingMessage(request), true); assert.strictEqual(isHTTPServer(server), true); -}, 2)); +}, 4)); const server = http.createServer(common.mustCall((req, res) => { - res.end('done'); -})); + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', common.mustCall(() => { + if (req.method === 'POST' && req.url === '/string-body') { + assert.strictEqual(Buffer.concat(chunks).toString(), 'foobar'); + } else if (req.method === 'POST' && req.url === '/binary-body') { + assert.deepStrictEqual(Buffer.concat(chunks), Buffer.from([0, 1, 2, 3])); + } else { + assert.strictEqual(req.method, 'GET'); + assert.strictEqual(req.url, '/'); + assert.strictEqual(Buffer.concat(chunks).byteLength, 0); + } + res.end('done'); + })); +}, 3)); server.listen(async () => { const { port } = server.address(); @@ -78,10 +111,33 @@ server.listen(async () => { await new Promise((resolve) => { invalidRequest.on('error', resolve); }); - http.get(`http://localhost:${port}`, (res) => { - res.resume(); - res.on('end', () => { - server.close(); + await new Promise((resolve, reject) => { + http.get(`http://localhost:${port}`, (res) => { + res.setEncoding('utf8'); + res.resume(); + res.on('end', resolve); + }).on('error', reject); + }); + await new Promise((resolve, reject) => { + const req = http.request(`http://localhost:${port}/string-body`, { + method: 'POST', + }, (res) => { + res.resume(); + res.on('end', resolve); + }); + req.on('error', reject); + req.write('foo'); + req.end('bar'); + }); + await new Promise((resolve, reject) => { + const req = http.request(`http://localhost:${port}/binary-body`, { + method: 'POST', + }, (res) => { + res.resume(); + res.on('end', resolve); }); + req.on('error', reject); + req.end(Buffer.from([0, 1, 2, 3])); }); + server.close(); }); diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index 88d717d83c896a..406dc52071d91b 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -22,6 +22,16 @@ const requestHeaders = { 'x-header1': ['value1', 'value2'] }; +const requestBodyHeaders = { + ...requestHeaders, + 'content-type': 'text/plain; charset=utf-8', +}; + +const binaryRequestBodyHeaders = { + ...requestHeaders, + 'content-type': 'application/octet-stream', +}; + const setResponseHeaders = (res) => { res.setHeader('server', 'node'); res.setHeader('etag', 12345); @@ -65,6 +75,17 @@ const handleRequest = (req, res) => { res.end('hello world\n'); }, kTimeout); break; + case '/text-body': { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', common.mustCall(() => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'foobar'); + setResponseHeaders(res); + res.writeHead(200); + res.end('hello world\n'); + })); + break; + } case '/echo-post': { const chunks = []; req.on('data', (chunk) => { @@ -81,6 +102,17 @@ const handleRequest = (req, res) => { }); break; } + case '/binary-body': { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', common.mustCall(() => { + assert.deepStrictEqual(Buffer.concat(chunks), Buffer.from([0, 1, 2, 3])); + setResponseHeaders(res); + res.writeHead(200); + res.end('hello world\n'); + })); + break; + } default: assert.fail(`Unexpected path: ${path}`); } @@ -118,6 +150,9 @@ function verifyRequestWillBeSent({ method, params }, expect) { assert.strictEqual(params.request.headers.cookie, 'k1=v1; k2=v2'); assert.strictEqual(params.request.headers.age, '1000'); assert.strictEqual(params.request.headers['x-header1'], 'value1, value2'); + if (expect.contentType) { + assert.strictEqual(params.request.headers['content-type'], expect.contentType); + } assert.strictEqual(typeof params.timestamp, 'number'); assert.strictEqual(typeof params.wallTime, 'number'); @@ -173,8 +208,11 @@ function verifyLoadingFailed({ method, params }) { assert.strictEqual(typeof params.errorText, 'string'); } -function verifyHttpResponse(response) { +function verifyHttpResponse(response, expectedBody = '\nhello world\n', responseEncoding) { assert.strictEqual(response.statusCode, 200); + if (responseEncoding) { + response.setEncoding(responseEncoding); + } const chunks = []; // Verifies that the inspector does not put the response into flowing mode. @@ -189,8 +227,8 @@ function verifyHttpResponse(response) { })); response.on('end', common.mustCall(() => { - const body = Buffer.concat(chunks).toString(); - assert.strictEqual(body, '\nhello world\n'); + const body = responseEncoding ? chunks.join('') : Buffer.concat(chunks).toString(); + assert.strictEqual(body, expectedBody); })); } @@ -203,6 +241,7 @@ function createRequestTracker(url, responseExpect, requestExpect = {}) { .then(([event]) => verifyRequestWillBeSent(event, { url, method: requestExpect.method, + contentType: requestExpect.contentType, })); const responseReceivedFuture = once(session, 'Network.responseReceived') @@ -345,7 +384,7 @@ async function testHttpsGet() { async function testHttpError() { const url = `http://${addresses.INVALID_HOST}/`; const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent') - .then(([event]) => verifyRequestWillBeSent(event, { url })); + .then(([event]) => verifyRequestWillBeSent(event, { url, method: 'GET' })); session.on('Network.responseReceived', common.mustNotCall()); session.on('Network.loadingFinished', common.mustNotCall()); @@ -364,7 +403,7 @@ async function testHttpError() { async function testHttpsError() { const url = `https://${addresses.INVALID_HOST}/`; const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent') - .then(([event]) => verifyRequestWillBeSent(event, { url })); + .then(([event]) => verifyRequestWillBeSent(event, { url, method: 'GET' })); session.on('Network.responseReceived', common.mustNotCall()); session.on('Network.loadingFinished', common.mustNotCall()); @@ -380,6 +419,98 @@ async function testHttpsError() { await loadingFailedFuture; } +async function makeHttpRequest( + requestModule, + options, + bodyWriter, + expectedBody = 'hello world\n', + responseEncoding, +) { + return new Promise((resolve, reject) => { + const req = requestModule.request(options, common.mustCall((res) => { + verifyHttpResponse(res, expectedBody, responseEncoding); + resolve(res); + })); + req.on('error', reject); + bodyWriter(req); + }); +} + +async function testTextBodyRequest({ requestModule, protocol, port, requestOptions }) { + const url = `${protocol}://127.0.0.1:${port}/text-body`; + requestOptions ??= {}; + const responseEncoding = protocol === 'http' ? 'utf8' : undefined; + const { + requestWillBeSentFuture, + responseReceivedFuture, + loadingFinishedFuture, + } = createRequestTracker(url, getDefaultResponseExpect(url), { + method: 'POST', + contentType: 'text/plain; charset=utf-8', + }); + + await makeHttpRequest(requestModule, { + host: '127.0.0.1', + port, + path: '/text-body', + method: 'POST', + ...requestOptions, + headers: requestBodyHeaders, + }, (req) => { + req.write('foo'); + req.end('bar'); + }, 'hello world\n', responseEncoding); + + await requestWillBeSentFuture; + const responseReceived = await responseReceivedFuture; + const loadingFinished = await loadingFinishedFuture; + + assert.ok(loadingFinished.timestamp >= responseReceived.timestamp); + + const requestBody = await session.post('Network.getRequestPostData', { + requestId: responseReceived.requestId, + }); + assert.strictEqual(requestBody.postData, 'foobar'); + + const responseBody = await session.post('Network.getResponseBody', { + requestId: responseReceived.requestId, + }); + assert.strictEqual(responseBody.base64Encoded, false); + assert.strictEqual(responseBody.body, 'hello world\n'); +} + +async function testBinaryBodyRequest() { + const url = `http://127.0.0.1:${httpServer.address().port}/binary-body`; + const { + requestWillBeSentFuture, + responseReceivedFuture, + loadingFinishedFuture, + } = createRequestTracker(url, getDefaultResponseExpect(url), { + method: 'POST', + contentType: 'application/octet-stream', + }); + + await makeHttpRequest(http, { + host: '127.0.0.1', + port: httpServer.address().port, + path: '/binary-body', + method: 'POST', + headers: binaryRequestBodyHeaders, + }, (req) => { + req.end(Buffer.from([0, 1, 2, 3])); + }, 'hello world\n'); + + await requestWillBeSentFuture; + const responseReceived = await responseReceivedFuture; + await loadingFinishedFuture; + + await assert.rejects(session.post('Network.getRequestPostData', { + requestId: responseReceived.requestId, + }), { + code: 'ERR_INSPECTOR_COMMAND', + }); +} + const testNetworkInspection = async () => { await testHttpGet(); session.removeAllListeners(); @@ -389,6 +520,21 @@ const testNetworkInspection = async () => { session.removeAllListeners(); await testHttpsGet(); session.removeAllListeners(); + await testTextBodyRequest({ + requestModule: http, + protocol: 'http', + port: httpServer.address().port, + }); + session.removeAllListeners(); + await testTextBodyRequest({ + requestModule: https, + protocol: 'https', + port: httpsServer.address().port, + requestOptions: { rejectUnauthorized: false }, + }); + session.removeAllListeners(); + await testBinaryBodyRequest(); + session.removeAllListeners(); await testHttpError(); session.removeAllListeners(); await testHttpsError(); From 214d435fb1223c0a72c8d04f221f144883508ae9 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Fri, 1 May 2026 01:29:49 +0800 Subject: [PATCH 02/18] test: wrap inspector HTTP handler in mustCall --- test/parallel/test-inspector-network-http.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index 406dc52071d91b..c2cf30cf81746a 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -62,7 +62,7 @@ function getPathName(req) { return new URL(req.url, `http://${req.headers.host}`).pathname; } -const handleRequest = (req, res) => { +const handleRequest = common.mustCall((req, res) => { const path = getPathName(req); switch (path) { case '/hello-world': @@ -116,7 +116,7 @@ const handleRequest = (req, res) => { default: assert.fail(`Unexpected path: ${path}`); } -}; +}, 5); const httpServer = http.createServer(handleRequest); @@ -130,7 +130,7 @@ const terminate = () => { httpServer.close(); httpsServer.close(); inspector.close(); -}; +}, 7); function findFrameInInitiator(scriptName, initiator) { const frame = initiator.stack.callFrames.find((it) => { From 3209955579dab80c7e8a9dcc3424edee31d3a2cc Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Fri, 1 May 2026 01:46:19 +0800 Subject: [PATCH 03/18] docs: add docs for dc http client --- doc/api/diagnostics_channel.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index b47f98ce64211c..79f9e4dec49461 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1576,6 +1576,21 @@ Unlike `http.client.request.start`, this event is emitted before the request has Emitted when client starts a request. +##### Event: `'http.client.request.bodyChunkSent'` + +* `request` {http.ClientRequest} +* `chunk` {Buffer|string|Uint8Array} +* `encoding` {string|null|undefined} + +Emitted when a chunk of the client request body is being sent. + +##### Event: `'http.client.request.bodySent'` + +* `request` {http.ClientRequest} + +Emitted after the client request body has been fully sent, if a request body +was written. + ##### Event: `'http.client.request.error'` * `request` {http.ClientRequest} @@ -1583,6 +1598,14 @@ Emitted when client starts a request. Emitted when an error occurs during a client request. +##### Event: `'http.client.response.bodyChunkReceived'` + +* `request` {http.ClientRequest} +* `response` {http.IncomingMessage} +* `chunk` {Buffer} + +Emitted when a chunk of the client response body is received. + ##### Event: `'http.client.response.finish'` * `request` {http.ClientRequest} From d6bb8410ef530a7807e68701d6dc736d7d4590d7 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Sun, 3 May 2026 12:23:38 +0800 Subject: [PATCH 04/18] test: fix inspector HTTP handler call counts --- test/parallel/test-inspector-network-http.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index c2cf30cf81746a..2955690e512ac6 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -116,7 +116,7 @@ const handleRequest = common.mustCall((req, res) => { default: assert.fail(`Unexpected path: ${path}`); } -}, 5); +}, 7); const httpServer = http.createServer(handleRequest); @@ -130,7 +130,7 @@ const terminate = () => { httpServer.close(); httpsServer.close(); inspector.close(); -}, 7); +}; function findFrameInInitiator(scriptName, initiator) { const frame = initiator.stack.callFrames.find((it) => { From 0dd8caf4e493d09b092ea058f5e1c0bfb0b891db Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Sun, 3 May 2026 14:49:15 +0800 Subject: [PATCH 05/18] test: inspector-network test hasPostData --- test/parallel/test-inspector-network-http.js | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index 2955690e512ac6..c3e02d0828dda4 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -145,6 +145,7 @@ function verifyRequestWillBeSent({ method, params }, expect) { assert.ok(params.requestId.startsWith('node-network-event-')); assert.strictEqual(params.request.url, expect.url); assert.strictEqual(params.request.method, expect.method ?? 'GET'); + assert.strictEqual(params.request.hasPostData, expect.hasPostData ?? false); assert.strictEqual(typeof params.request.headers, 'object'); assert.strictEqual(params.request.headers['accept-language'], 'en-US'); assert.strictEqual(params.request.headers.cookie, 'k1=v1; k2=v2'); @@ -241,6 +242,7 @@ function createRequestTracker(url, responseExpect, requestExpect = {}) { .then(([event]) => verifyRequestWillBeSent(event, { url, method: requestExpect.method, + hasPostData: requestExpect.hasPostData, contentType: requestExpect.contentType, })); @@ -271,7 +273,9 @@ async function testHttpGet() { requestWillBeSentFuture, responseReceivedFuture, loadingFinishedFuture, - } = createRequestTracker(url, getDefaultResponseExpect(url)); + } = createRequestTracker(url, getDefaultResponseExpect(url), { + hasPostData: true, + }); http.get({ host: '127.0.0.1', @@ -295,7 +299,9 @@ async function testHttpGetWithAbsoluteUrlPath() { requestWillBeSentFuture, responseReceivedFuture, loadingFinishedFuture, - } = createRequestTracker(url, getDefaultResponseExpect(url)); + } = createRequestTracker(url, getDefaultResponseExpect(url), { + hasPostData: true, + }); http.get({ host: '127.0.0.1', @@ -326,6 +332,8 @@ async function testHttpPostWithAbsoluteUrlPath() { charset: 'utf-8', }, { method: 'POST', + hasPostData: true, + contentType: 'application/json', }); const responsePromise = new Promise((resolve, reject) => { @@ -362,7 +370,9 @@ async function testHttpsGet() { requestWillBeSentFuture, responseReceivedFuture, loadingFinishedFuture, - } = createRequestTracker(url, getDefaultResponseExpect(url)); + } = createRequestTracker(url, getDefaultResponseExpect(url), { + hasPostData: true, + }); https.get({ host: '127.0.0.1', @@ -384,7 +394,11 @@ async function testHttpsGet() { async function testHttpError() { const url = `http://${addresses.INVALID_HOST}/`; const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent') - .then(([event]) => verifyRequestWillBeSent(event, { url, method: 'GET' })); + .then(([event]) => verifyRequestWillBeSent(event, { + url, + method: 'GET', + hasPostData: true, + })); session.on('Network.responseReceived', common.mustNotCall()); session.on('Network.loadingFinished', common.mustNotCall()); @@ -403,7 +417,11 @@ async function testHttpError() { async function testHttpsError() { const url = `https://${addresses.INVALID_HOST}/`; const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent') - .then(([event]) => verifyRequestWillBeSent(event, { url, method: 'GET' })); + .then(([event]) => verifyRequestWillBeSent(event, { + url, + method: 'GET', + hasPostData: true, + })); session.on('Network.responseReceived', common.mustNotCall()); session.on('Network.loadingFinished', common.mustNotCall()); @@ -446,6 +464,7 @@ async function testTextBodyRequest({ requestModule, protocol, port, requestOptio loadingFinishedFuture, } = createRequestTracker(url, getDefaultResponseExpect(url), { method: 'POST', + hasPostData: true, contentType: 'text/plain; charset=utf-8', }); @@ -487,6 +506,7 @@ async function testBinaryBodyRequest() { loadingFinishedFuture, } = createRequestTracker(url, getDefaultResponseExpect(url), { method: 'POST', + hasPostData: true, contentType: 'application/octet-stream', }); From ba9f78866afaffd4ef126ee1769e2722d84f7ac2 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Sun, 3 May 2026 20:30:36 +0800 Subject: [PATCH 06/18] test: cover untracked inspector HTTP body events --- test/parallel/test-inspector-network-http.js | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index c3e02d0828dda4..c3aae2ffa8077a 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -5,7 +5,9 @@ const common = require('../common'); common.skipIfInspectorDisabled(); const assert = require('node:assert'); +const dc = require('node:diagnostics_channel'); const { once } = require('node:events'); +const { setImmediate: waitForTurn } = require('node:timers/promises'); const { addresses } = require('../common/internet'); const fixtures = require('../common/fixtures'); const http = require('node:http'); @@ -267,6 +269,31 @@ async function assertResponseBody(responseReceived, expectedBody, expectedBase64 assert.strictEqual(responseBody.body, expectedBody); } +async function testUntrackedBodyEventsAreIgnored() { + const onDataSent = common.mustNotCall(); + const onDataReceived = common.mustNotCall(); + session.on('Network.dataSent', onDataSent); + session.on('Network.dataReceived', onDataReceived); + + dc.channel('http.client.request.bodyChunkSent').publish({ + request: {}, + chunk: 'ignored', + encoding: 'utf8', + }); + dc.channel('http.client.request.bodySent').publish({ + request: {}, + }); + dc.channel('http.client.response.bodyChunkReceived').publish({ + request: {}, + chunk: Buffer.from('ignored'), + }); + + await waitForTurn(); + + session.off('Network.dataSent', onDataSent); + session.off('Network.dataReceived', onDataReceived); +} + async function testHttpGet() { const url = `http://127.0.0.1:${httpServer.address().port}/hello-world`; const { @@ -532,6 +559,8 @@ async function testBinaryBodyRequest() { } const testNetworkInspection = async () => { + await testUntrackedBodyEventsAreIgnored(); + session.removeAllListeners(); await testHttpGet(); session.removeAllListeners(); await testHttpGetWithAbsoluteUrlPath(); From 98830e20bd4396b1267655b7d7cc421b6388fa31 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 15:18:07 +0800 Subject: [PATCH 07/18] inspector: fix requestWillBeSent body state --- lib/internal/inspector/network_http.js | 57 ++++++- src/inspector/network_agent.cc | 155 ++++++++++++++++++- src/inspector/network_agent.h | 2 + test/parallel/test-inspector-network-http.js | 14 +- 4 files changed, 208 insertions(+), 20 deletions(-) diff --git a/lib/internal/inspector/network_http.js b/lib/internal/inspector/network_http.js index 52614540ad16d5..42ca7c976cd46f 100644 --- a/lib/internal/inspector/network_http.js +++ b/lib/internal/inspector/network_http.js @@ -2,7 +2,9 @@ const { ArrayIsArray, + ArrayPrototypePush, DateNow, + MathMax, ObjectEntries, String, StringPrototypeStartsWith, @@ -17,10 +19,13 @@ const { registerDiagnosticChannels, sniffMimeType, } = require('internal/inspector/network'); +const { getStructuredStack } = require('internal/util'); const { Network } = require('inspector'); const { Buffer } = require('buffer'); const kRequestUrl = Symbol('kRequestUrl'); +const kRequestWillBeSent = Symbol('kRequestWillBeSent'); +const kInitiator = Symbol('kInitiator'); function isAbsoluteURLPath(path) { return typeof path === 'string' && @@ -67,38 +72,80 @@ const convertHeaderObject = (headers = {}) => { return [dict, host, charset, mimeType]; }; +function createInitiator() { + const callSites = getStructuredStack(); + const callFrames = []; + for (let i = 0; i < callSites.length; i++) { + const callSite = callSites[i]; + ArrayPrototypePush(callFrames, { + functionName: callSite.getFunctionName() ?? callSite.getMethodName() ?? '', + scriptId: '', + url: callSite.getScriptNameOrSourceURL() ?? callSite.getFileName() ?? '', + lineNumber: MathMax((callSite.getLineNumber() ?? 1) - 1, 0), + columnNumber: MathMax((callSite.getColumnNumber() ?? 1) - 1, 0), + }); + } + return { + type: 'script', + stack: { callFrames }, + }; +} + /** - * When a client request is created, emit Network.requestWillBeSent event. - * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent + * When a client request is created, assign its inspector request id. * @param {{ request: import('http').ClientRequest }} event */ function onClientRequestCreated({ request }) { request[kInspectorRequestId] = getNextRequestId(); + request[kInitiator] = createInitiator(); +} + +/** + * Emit Network.requestWillBeSent once the request body state is known. + * @param {import('http').ClientRequest} request + * @param {boolean} hasPostData + */ +function emitRequestWillBeSent(request, hasPostData) { + if (request[kRequestWillBeSent] || + typeof request[kInspectorRequestId] !== 'string') { + return; + } const { 0: headers, 1: host, 2: charset } = convertHeaderObject(request.getHeaders()); const url = getRequestURL(request, host); request[kRequestUrl] = url; + request[kRequestWillBeSent] = true; Network.requestWillBeSent({ requestId: request[kInspectorRequestId], timestamp: getMonotonicTime(), wallTime: DateNow(), charset, + initiator: request[kInitiator], request: { url, method: request.method, headers, - hasPostData: !request.writableEnded, + hasPostData, }, }); } +/** + * When a client request starts without a body, emit Network.requestWillBeSent. + * @param {{ request: import('http').ClientRequest }} event + */ +function onClientRequestStart({ request }) { + emitRequestWillBeSent(request, false); +} + /** * When a client request errors, emit Network.loadingFailed event. * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed * @param {{ request: import('http').ClientRequest, error: any }} event */ function onClientRequestError({ request, error }) { + emitRequestWillBeSent(request, false); if (typeof request[kInspectorRequestId] !== 'string') { return; } @@ -121,6 +168,8 @@ function onClientRequestBodyChunkSent({ request, chunk, encoding }) { return; } + emitRequestWillBeSent(request, true); + const buffer = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : Buffer.from(chunk); Network.dataSent({ requestId: request[kInspectorRequestId], @@ -174,6 +223,7 @@ function onClientResponseBodyChunkReceived({ request, chunk }) { * @param {{ request: import('http').ClientRequest, error: any }} event */ function onClientResponseFinish({ request, response }) { + emitRequestWillBeSent(request, false); if (typeof request[kInspectorRequestId] !== 'string') { return; } @@ -205,6 +255,7 @@ function onClientResponseFinish({ request, response }) { module.exports = registerDiagnosticChannels([ ['http.client.request.created', onClientRequestCreated], + ['http.client.request.start', onClientRequestStart], ['http.client.request.bodyChunkSent', onClientRequestBodyChunkSent], ['http.client.request.bodySent', onClientRequestBodySent], ['http.client.request.error', onClientRequestError], diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index ace8ba52287186..c28de96b4ef408 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -6,6 +6,7 @@ #include "inspector/network_resource_manager.h" #include "inspector/protocol_helper.h" #include "network_inspector.h" +#include "node/inspector/protocol/Runtime.h" #include "node_metadata.h" #include "util-inl.h" #include "uv.h" @@ -15,9 +16,12 @@ namespace node { namespace inspector { +using v8::Array; +using v8::Context; using v8::HandleScope; using v8::Isolate; using v8::Local; +using v8::MaybeLocal; using v8::Object; using v8::Uint8Array; using v8::Value; @@ -29,6 +33,76 @@ static void ThrowEventError(v8::Isolate* isolate, const std::string& message) { v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked())); } +static MaybeLocal GetProperty(Local context, + Local object, + Local key) { + return object->Get(context, key); +} + +static std::unique_ptr V8ToProtocolValue( + Local context, + Local value) { + Isolate* isolate = Isolate::GetCurrent(); + if (value->IsNullOrUndefined()) { + return protocol::Value::null(); + } + if (value->IsBoolean()) { + return protocol::FundamentalValue::create(value.As()->Value()); + } + if (value->IsInt32()) { + return protocol::FundamentalValue::create(value.As()->Value()); + } + if (value->IsNumber()) { + return protocol::FundamentalValue::create(value.As()->Value()); + } + if (value->IsString()) { + return protocol::StringValue::create(ToProtocolString(isolate, value)); + } + if (value->IsArray()) { + Local array = value.As(); + std::unique_ptr list = protocol::ListValue::create(); + list->reserve(array->Length()); + for (uint32_t i = 0; i < array->Length(); i++) { + Local element; + if (!array->Get(context, i).ToLocal(&element)) { + return nullptr; + } + std::unique_ptr protocol_value = + V8ToProtocolValue(context, element); + if (!protocol_value) { + return nullptr; + } + list->pushValue(std::move(protocol_value)); + } + return list; + } + if (value->IsObject()) { + Local object = value.As(); + Local property_names; + if (!object->GetOwnPropertyNames(context).ToLocal(&property_names)) { + return nullptr; + } + std::unique_ptr dict = + protocol::DictionaryValue::create(); + for (uint32_t i = 0; i < property_names->Length(); i++) { + Local key; + Local property; + if (!property_names->Get(context, i).ToLocal(&key) || + !GetProperty(context, object, key).ToLocal(&property)) { + return nullptr; + } + std::unique_ptr protocol_value = + V8ToProtocolValue(context, property); + if (!protocol_value) { + return nullptr; + } + dict->setValue(ToProtocolString(isolate, key), std::move(protocol_value)); + } + return dict; + } + return nullptr; +} + // Create a protocol::Network::Headers from the v8 object. std::unique_ptr NetworkAgent::createHeadersFromObject(v8::Local context, @@ -65,6 +139,67 @@ NetworkAgent::createHeadersFromObject(v8::Local context, return std::make_unique(std::move(dict)); } +std::unique_ptr +NetworkAgent::createInitiatorFromObject(v8::Local context, + Local initiator_obj) { + HandleScope handle_scope(Isolate::GetCurrent()); + Isolate* isolate = env_->isolate(); + + protocol::String type; + if (!ObjectGetProtocolString(context, initiator_obj, "type").To(&type)) { + ThrowEventError(isolate, "Missing initiator.type in event"); + return {}; + } + + std::unique_ptr initiator = + protocol::Network::Initiator::create().setType(type).build(); + + Local stack_obj; + if (ObjectGetObject(context, initiator_obj, "stack").ToLocal(&stack_obj)) { + std::unique_ptr stack_value = + V8ToProtocolValue(context, stack_obj); + if (!stack_value) { + ThrowEventError(isolate, "Invalid initiator.stack in event"); + return {}; + } + + protocol::ErrorSupport errors; + std::unique_ptr stack = + protocol::ValueConversions< + v8_inspector::protocol::Runtime::API::StackTrace>::fromValue( + stack_value.get(), &errors); + if (!stack) { + ThrowEventError(isolate, "Invalid initiator.stack in event"); + return {}; + } + initiator->setStack(std::move(stack)); + } + + protocol::String url; + if (ObjectGetProtocolString(context, initiator_obj, "url").To(&url)) { + initiator->setUrl(url); + } + + double line_number; + if (ObjectGetDouble(context, initiator_obj, "lineNumber").To(&line_number)) { + initiator->setLineNumber(line_number); + } + + double column_number; + if (ObjectGetDouble(context, initiator_obj, "columnNumber") + .To(&column_number)) { + initiator->setColumnNumber(column_number); + } + + protocol::String request_id; + if (ObjectGetProtocolString(context, initiator_obj, "requestId") + .To(&request_id)) { + initiator->setRequestId(request_id); + } + + return initiator; +} + // Create a protocol::Network::Request from the v8 object. std::unique_ptr NetworkAgent::createRequestFromObject(v8::Local context, @@ -460,12 +595,20 @@ void NetworkAgent::requestWillBeSent(v8::Local context, return; } - std::unique_ptr initiator = - protocol::Network::Initiator::create() - .setType(protocol::Network::Initiator::TypeEnum::Script) - .setStack( - v8_inspector_->captureStackTrace(true)->buildInspectorObject(0)) - .build(); + std::unique_ptr initiator; + Local initiator_obj; + if (ObjectGetObject(context, params, "initiator").ToLocal(&initiator_obj)) { + initiator = createInitiatorFromObject(context, initiator_obj); + if (!initiator) { + return; + } + } else { + initiator = protocol::Network::Initiator::create() + .setType(protocol::Network::Initiator::TypeEnum::Script) + .setStack(v8_inspector_->captureStackTrace(true) + ->buildInspectorObject(0)) + .build(); + } if (requests_.contains(request_id)) { // Duplicate entry, ignore it. diff --git a/src/inspector/network_agent.h b/src/inspector/network_agent.h index 2136a45baf45f6..a16e1781e4c9c6 100644 --- a/src/inspector/network_agent.h +++ b/src/inspector/network_agent.h @@ -81,6 +81,8 @@ class NetworkAgent : public protocol::Network::Backend { private: std::unique_ptr createHeadersFromObject( v8::Local context, v8::Local headers_obj); + std::unique_ptr createInitiatorFromObject( + v8::Local context, v8::Local initiator_obj); std::unique_ptr createRequestFromObject( v8::Local context, v8::Local request); std::unique_ptr createResponseFromObject( diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index c3aae2ffa8077a..6bdc26fb6dc98d 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -300,9 +300,7 @@ async function testHttpGet() { requestWillBeSentFuture, responseReceivedFuture, loadingFinishedFuture, - } = createRequestTracker(url, getDefaultResponseExpect(url), { - hasPostData: true, - }); + } = createRequestTracker(url, getDefaultResponseExpect(url)); http.get({ host: '127.0.0.1', @@ -326,9 +324,7 @@ async function testHttpGetWithAbsoluteUrlPath() { requestWillBeSentFuture, responseReceivedFuture, loadingFinishedFuture, - } = createRequestTracker(url, getDefaultResponseExpect(url), { - hasPostData: true, - }); + } = createRequestTracker(url, getDefaultResponseExpect(url)); http.get({ host: '127.0.0.1', @@ -397,9 +393,7 @@ async function testHttpsGet() { requestWillBeSentFuture, responseReceivedFuture, loadingFinishedFuture, - } = createRequestTracker(url, getDefaultResponseExpect(url), { - hasPostData: true, - }); + } = createRequestTracker(url, getDefaultResponseExpect(url)); https.get({ host: '127.0.0.1', @@ -424,7 +418,6 @@ async function testHttpError() { .then(([event]) => verifyRequestWillBeSent(event, { url, method: 'GET', - hasPostData: true, })); session.on('Network.responseReceived', common.mustNotCall()); session.on('Network.loadingFinished', common.mustNotCall()); @@ -447,7 +440,6 @@ async function testHttpsError() { .then(([event]) => verifyRequestWillBeSent(event, { url, method: 'GET', - hasPostData: true, })); session.on('Network.responseReceived', common.mustNotCall()); session.on('Network.loadingFinished', common.mustNotCall()); From 452c083a035637cb81c90aca18fd2411bf216222 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 15:48:12 +0800 Subject: [PATCH 08/18] inspector: format network_agent.cc --- src/inspector/network_agent.cc | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index c28de96b4ef408..0996dfbac95b90 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -40,8 +40,7 @@ static MaybeLocal GetProperty(Local context, } static std::unique_ptr V8ToProtocolValue( - Local context, - Local value) { + Local context, Local value) { Isolate* isolate = Isolate::GetCurrent(); if (value->IsNullOrUndefined()) { return protocol::Value::null(); @@ -165,9 +164,9 @@ NetworkAgent::createInitiatorFromObject(v8::Local context, protocol::ErrorSupport errors; std::unique_ptr stack = - protocol::ValueConversions< - v8_inspector::protocol::Runtime::API::StackTrace>::fromValue( - stack_value.get(), &errors); + protocol::ValueConversions::fromValue(stack_value.get(), + &errors); if (!stack) { ThrowEventError(isolate, "Invalid initiator.stack in event"); return {}; @@ -603,11 +602,12 @@ void NetworkAgent::requestWillBeSent(v8::Local context, return; } } else { - initiator = protocol::Network::Initiator::create() - .setType(protocol::Network::Initiator::TypeEnum::Script) - .setStack(v8_inspector_->captureStackTrace(true) - ->buildInspectorObject(0)) - .build(); + initiator = + protocol::Network::Initiator::create() + .setType(protocol::Network::Initiator::TypeEnum::Script) + .setStack( + v8_inspector_->captureStackTrace(true)->buildInspectorObject(0)) + .build(); } if (requests_.contains(request_id)) { From ef88ee8192b6164b584f59f2551538c6beb237c0 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 18:36:10 +0800 Subject: [PATCH 09/18] test: cover network inspector initiator parsing --- .../test-inspector-network-arbitrary-data.js | 211 +++++++++++++++++- 1 file changed, 206 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-inspector-network-arbitrary-data.js b/test/parallel/test-inspector-network-arbitrary-data.js index 2df76010f53082..ee7375bf373e0c 100644 --- a/test/parallel/test-inspector-network-arbitrary-data.js +++ b/test/parallel/test-inspector-network-arbitrary-data.js @@ -9,11 +9,51 @@ const { Network } = require('node:inspector'); const test = require('node:test'); const assert = require('node:assert'); const { waitUntil } = require('../common/inspector-helper'); +const { setImmediate: waitForTurn } = require('node:timers/promises'); const session = new inspector.Session(); session.connect(); +function createRequestPayload(overrides = {}) { + return { + requestId: '1', + timestamp: 1, + wallTime: 1, + request: { + url: 'https://example.com', + method: 'GET', + headers: { + mKey: 'mValue', + }, + }, + ...overrides, + }; +} + +async function assertInvalidInitiatorStack(stack, requestId) { + session.removeAllListeners(); + await session.post('Network.enable'); + + session.on('Network.requestWillBeSent', common.mustNotCall()); + + assert.throws(() => { + Network.requestWillBeSent(createRequestPayload({ + requestId, + initiator: { + type: 'script', + stack, + }, + })); + }, { + name: 'TypeError', + message: 'Invalid initiator.stack in event', + }); + + await waitForTurn(); +} + test('should emit Network.requestWillBeSent with unicode', async () => { + session.removeAllListeners(); await session.post('Network.enable'); const expectedValue = 'CJK 汉字 🍱 🧑‍🧑‍🧒‍🧒'; @@ -24,10 +64,7 @@ test('should emit Network.requestWillBeSent with unicode', async () => { assert.strictEqual(event.params.request.headers.mKey, expectedValue); }); - Network.requestWillBeSent({ - requestId: '1', - timestamp: 1, - wallTime: 1, + Network.requestWillBeSent(createRequestPayload({ request: { url: expectedValue, method: expectedValue, @@ -35,7 +72,171 @@ test('should emit Network.requestWillBeSent with unicode', async () => { mKey: expectedValue, }, }, - }); + })); + + await requestWillBeSentFuture; +}); + +test('should emit Network.requestWillBeSent with custom initiator', async () => { + session.removeAllListeners(); + await session.post('Network.enable'); + + const requestWillBeSentFuture = waitUntil(session, 'Network.requestWillBeSent') + .then(([event]) => { + const { initiator } = event.params; + assert.strictEqual(initiator.type, 'parser'); + assert.strictEqual(initiator.url, 'node:https://initiator.test/app.js'); + assert.strictEqual(initiator.lineNumber, 12); + assert.strictEqual(initiator.columnNumber, 34); + assert.strictEqual(initiator.requestId, 'parent-request-id'); + assert.strictEqual(initiator.stack.description, 'custom stack'); + assert.deepStrictEqual(initiator.stack.callFrames, [{ + functionName: 'run', + scriptId: '99', + url: 'file:///custom-frame.js', + lineNumber: 3, + columnNumber: 5, + }]); + assert.deepStrictEqual(initiator.stack.parent.callFrames, [{ + functionName: 'parentRun', + scriptId: '100', + url: 'file:///parent-frame.js', + lineNumber: 8, + columnNumber: 13, + }]); + assert.deepStrictEqual(initiator.stack.parentId, { + id: 'async-stack-id', + debuggerId: 'debugger-1', + }); + }); + + Network.requestWillBeSent(createRequestPayload({ + requestId: 'custom-initiator-request', + initiator: { + type: 'parser', + url: 'node:https://initiator.test/app.js', + lineNumber: 12, + columnNumber: 34, + requestId: 'parent-request-id', + stack: { + description: 'custom stack', + callFrames: [{ + functionName: 'run', + scriptId: '99', + url: 'file:///custom-frame.js', + lineNumber: 3, + columnNumber: 5, + }], + parent: { + callFrames: [{ + functionName: 'parentRun', + scriptId: '100', + url: 'file:///parent-frame.js', + lineNumber: 8, + columnNumber: 13, + }], + extraNumber: 1.5, + extraBoolean: true, + extraNull: null, + }, + parentId: { + id: 'async-stack-id', + debuggerId: 'debugger-1', + }, + extraArray: ['frame', 1, false, null, { nested: 'value' }], + }, + }, + })); await requestWillBeSentFuture; }); + +test('should throw if initiator.type is missing', async () => { + session.removeAllListeners(); + await session.post('Network.enable'); + + session.on('Network.requestWillBeSent', common.mustNotCall()); + + assert.throws(() => { + Network.requestWillBeSent(createRequestPayload({ + requestId: 'missing-initiator-type', + initiator: { + stack: { + callFrames: [], + }, + }, + })); + }, { + name: 'TypeError', + message: 'Missing initiator.type in event', + }); + + await waitForTurn(); +}); + +test('should throw if initiator.stack is invalid', async () => { + await assertInvalidInitiatorStack({ + callFrames: [], + unsupportedValue: 1n, + }, 'invalid-initiator-stack'); +}); + +test('should throw if initiator.stack contains an invalid array element', + async () => { + await assertInvalidInitiatorStack({ + callFrames: [], + extraArray: [1n], + }, 'invalid-initiator-stack-array-element'); + }); + +test('should throw if initiator.stack contains an array accessor that throws', + async () => { + const extraArray = []; + Object.defineProperty(extraArray, 0, { + enumerable: true, + get() { + throw new Error('array getter boom'); + }, + }); + extraArray.length = 1; + + await assertInvalidInitiatorStack({ + callFrames: [], + extraArray, + }, 'invalid-initiator-stack-array-getter'); + }); + +test('should throw if initiator.stack has a property accessor that throws', + async () => { + const stack = { callFrames: [] }; + Object.defineProperty(stack, 'broken', { + enumerable: true, + get() { + throw new Error('getter boom'); + }, + }); + + await assertInvalidInitiatorStack( + stack, + 'invalid-initiator-stack-property-getter', + ); + }); + +test('should throw if initiator.stack property enumeration throws', async () => { + const stack = new Proxy({ callFrames: [] }, { + ownKeys() { + throw new Error('ownKeys boom'); + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + }; + }, + }); + + await assertInvalidInitiatorStack( + stack, + 'invalid-initiator-stack-own-keys', + ); +}); From 9c3ad9ef9353e23881ec5e9def2a17646ce26741 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 20:55:55 +0800 Subject: [PATCH 10/18] inspector,test: reject malformed initiator stack traces --- node.gyp | 1 + src/inspector/network_agent.cc | 2 +- test/cctest/inspector/test_node_protocol.cc | 22 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/node.gyp b/node.gyp index b129c3db8d88c1..cb89acee7eaaf2 100644 --- a/node.gyp +++ b/node.gyp @@ -1418,6 +1418,7 @@ # TODO(legendecas): make node_inspector.gypi a dependable target. '<(SHARED_INTERMEDIATE_DIR)', # for inspector '<(SHARED_INTERMEDIATE_DIR)/src', # for inspector + '<(SHARED_INTERMEDIATE_DIR)/inspector-generated-output-root/include', ], 'dependencies': [ 'deps/inspector_protocol/inspector_protocol.gyp:crdtp', diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 0996dfbac95b90..d92bfaeb74fe17 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -167,7 +167,7 @@ NetworkAgent::createInitiatorFromObject(v8::Local context, protocol::ValueConversions::fromValue(stack_value.get(), &errors); - if (!stack) { + if (!stack || !errors.Errors().empty()) { ThrowEventError(isolate, "Invalid initiator.stack in event"); return {}; } diff --git a/test/cctest/inspector/test_node_protocol.cc b/test/cctest/inspector/test_node_protocol.cc index 57e42e51adb859..033e4a4b687a32 100644 --- a/test/cctest/inspector/test_node_protocol.cc +++ b/test/cctest/inspector/test_node_protocol.cc @@ -2,12 +2,22 @@ #include "gtest/gtest.h" #include "inspector/node_json.h" #include "node/inspector/protocol/Protocol.h" +#include "node/inspector/protocol/Runtime.h" namespace node { namespace inspector { namespace protocol { namespace { +class MalformedSerializedValue : public Value { + public: + MalformedSerializedValue() : Value(TypeObject) {} + + void AppendSerialized(std::vector* out) const override { + out->push_back(0xff); + } +}; + TEST(InspectorProtocol, Utf8StringSerDes) { constexpr const char* kKey = "unicode_key"; constexpr const char* kValue = "CJK 汉字 🍱 🧑‍🧑‍🧒‍🧒"; @@ -27,6 +37,18 @@ TEST(InspectorProtocol, Utf8StringSerDes) { CHECK_EQ(parsed_value, std::string(kValue)); } +TEST(InspectorProtocol, StackTraceImportRejectsMalformedBinary) { + MalformedSerializedValue value; + ErrorSupport errors; + + auto stack = + ValueConversions:: + fromValue(&value, &errors); + + EXPECT_EQ(stack, nullptr); + EXPECT_FALSE(errors.Errors().empty()); +} + } // namespace } // namespace protocol } // namespace inspector From dac18b6619f96cfb2a06384865e1f34dab9659e6 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 21:17:26 +0800 Subject: [PATCH 11/18] test: fix inspector protocol cctest formatting --- test/cctest/inspector/test_node_protocol.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/cctest/inspector/test_node_protocol.cc b/test/cctest/inspector/test_node_protocol.cc index 033e4a4b687a32..2422b015a34c38 100644 --- a/test/cctest/inspector/test_node_protocol.cc +++ b/test/cctest/inspector/test_node_protocol.cc @@ -41,9 +41,9 @@ TEST(InspectorProtocol, StackTraceImportRejectsMalformedBinary) { MalformedSerializedValue value; ErrorSupport errors; - auto stack = - ValueConversions:: - fromValue(&value, &errors); + auto stack = ValueConversions< + v8_inspector::protocol::Runtime::API::StackTrace>::fromValue(&value, + &errors); EXPECT_EQ(stack, nullptr); EXPECT_FALSE(errors.Errors().empty()); From 37a881a72611105904ed81a62e50d2c543770f45 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 22:14:59 +0800 Subject: [PATCH 12/18] test: cover invalid inspector network initiator stack --- test/cctest/inspector/test_node_protocol.cc | 22 ------------------- ...st-inspector-emit-protocol-event-errors.js | 13 +++++++++++ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/test/cctest/inspector/test_node_protocol.cc b/test/cctest/inspector/test_node_protocol.cc index 2422b015a34c38..57e42e51adb859 100644 --- a/test/cctest/inspector/test_node_protocol.cc +++ b/test/cctest/inspector/test_node_protocol.cc @@ -2,22 +2,12 @@ #include "gtest/gtest.h" #include "inspector/node_json.h" #include "node/inspector/protocol/Protocol.h" -#include "node/inspector/protocol/Runtime.h" namespace node { namespace inspector { namespace protocol { namespace { -class MalformedSerializedValue : public Value { - public: - MalformedSerializedValue() : Value(TypeObject) {} - - void AppendSerialized(std::vector* out) const override { - out->push_back(0xff); - } -}; - TEST(InspectorProtocol, Utf8StringSerDes) { constexpr const char* kKey = "unicode_key"; constexpr const char* kValue = "CJK 汉字 🍱 🧑‍🧑‍🧒‍🧒"; @@ -37,18 +27,6 @@ TEST(InspectorProtocol, Utf8StringSerDes) { CHECK_EQ(parsed_value, std::string(kValue)); } -TEST(InspectorProtocol, StackTraceImportRejectsMalformedBinary) { - MalformedSerializedValue value; - ErrorSupport errors; - - auto stack = ValueConversions< - v8_inspector::protocol::Runtime::API::StackTrace>::fromValue(&value, - &errors); - - EXPECT_EQ(stack, nullptr); - EXPECT_FALSE(errors.Errors().empty()); -} - } // namespace } // namespace protocol } // namespace inspector diff --git a/test/parallel/test-inspector-emit-protocol-event-errors.js b/test/parallel/test-inspector-emit-protocol-event-errors.js index 1a76a491c2195c..5bae0d08d4efc0 100644 --- a/test/parallel/test-inspector-emit-protocol-event-errors.js +++ b/test/parallel/test-inspector-emit-protocol-event-errors.js @@ -171,6 +171,19 @@ const NETWORK_ERROR_CASES = [ networkRequest({ request: omit(networkRequest().request, 'headers') }), 'Missing request.headers in event', ], + [ + 'requestWillBeSent', + networkRequest({ + initiator: { + type: 'script', + stack: { + callFrames: [], + unsupportedValue: 1n, + }, + }, + }), + 'Invalid initiator.stack in event', + ], [ 'responseReceived', From b88102536aef1227fa6881290a574401f0a6f92a Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Mon, 4 May 2026 23:42:25 +0800 Subject: [PATCH 13/18] inspector: document network initiator validation --- src/inspector/network_agent.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index d92bfaeb74fe17..e5dd92ab934398 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -39,6 +39,8 @@ static MaybeLocal GetProperty(Local context, return object->Get(context, key); } +// Convert JS-provided event payloads into protocol values so existing inspector +// protocol schema validators can reject malformed structured fields. static std::unique_ptr V8ToProtocolValue( Local context, Local value) { Isolate* isolate = Isolate::GetCurrent(); @@ -155,6 +157,8 @@ NetworkAgent::createInitiatorFromObject(v8::Local context, Local stack_obj; if (ObjectGetObject(context, initiator_obj, "stack").ToLocal(&stack_obj)) { + // `initiator.stack` is passed in from JS diagnostics channels. Validate it + // against the Runtime.StackTrace schema before forwarding it to frontends. std::unique_ptr stack_value = V8ToProtocolValue(context, stack_obj); if (!stack_value) { From a798ab582b85794dcc0c16d41becfcc7aebd5e12 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Tue, 5 May 2026 00:28:00 +0800 Subject: [PATCH 14/18] inspector: drop unreachable initiator.stack schema --- src/inspector/network_agent.cc | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index e5dd92ab934398..8f8bd4a73a4aed 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -157,8 +157,6 @@ NetworkAgent::createInitiatorFromObject(v8::Local context, Local stack_obj; if (ObjectGetObject(context, initiator_obj, "stack").ToLocal(&stack_obj)) { - // `initiator.stack` is passed in from JS diagnostics channels. Validate it - // against the Runtime.StackTrace schema before forwarding it to frontends. std::unique_ptr stack_value = V8ToProtocolValue(context, stack_obj); if (!stack_value) { @@ -167,15 +165,10 @@ NetworkAgent::createInitiatorFromObject(v8::Local context, } protocol::ErrorSupport errors; - std::unique_ptr stack = + initiator->setStack( protocol::ValueConversions::fromValue(stack_value.get(), - &errors); - if (!stack || !errors.Errors().empty()) { - ThrowEventError(isolate, "Invalid initiator.stack in event"); - return {}; - } - initiator->setStack(std::move(stack)); + &errors)); } protocol::String url; From 3fcb13d070e87920e91319fa7daf4bbe7516ae04 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Tue, 5 May 2026 11:38:04 +0800 Subject: [PATCH 15/18] test: add more case --- ...st-inspector-emit-protocol-event-errors.js | 20 ++++++++ .../test-inspector-emit-protocol-event.js | 47 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/test/parallel/test-inspector-emit-protocol-event-errors.js b/test/parallel/test-inspector-emit-protocol-event-errors.js index 5bae0d08d4efc0..74cde3e8e85d51 100644 --- a/test/parallel/test-inspector-emit-protocol-event-errors.js +++ b/test/parallel/test-inspector-emit-protocol-event-errors.js @@ -184,6 +184,26 @@ const NETWORK_ERROR_CASES = [ }), 'Invalid initiator.stack in event', ], + [ + 'requestWillBeSent', + networkRequest({ + initiator: { + type: 'script', + stack: (() => { + const stack = {}; + Object.defineProperty(stack, 'callFrames', { + enumerable: true, + configurable: true, + get() { + throw new Error('boom'); + }, + }); + return stack; + })(), + }, + }), + 'Invalid initiator.stack in event', + ], [ 'responseReceived', diff --git a/test/parallel/test-inspector-emit-protocol-event.js b/test/parallel/test-inspector-emit-protocol-event.js index 567c92e3eeba6a..a1af972f2353d3 100644 --- a/test/parallel/test-inspector-emit-protocol-event.js +++ b/test/parallel/test-inspector-emit-protocol-event.js @@ -194,6 +194,53 @@ for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { } } + // Verify a user-supplied initiator (with stack) is preserved end-to-end. + // Covers the success body of `if (ObjectGetObject(... "stack"))` in + // NetworkAgent::createInitiatorFromObject (network_agent.cc L159). + session.removeAllListeners('Network.requestWillBeSent'); + const userInitiator = { + type: 'script', + stack: { callFrames: [] }, + url: 'https://nodejs.org/test.js', + lineNumber: 12, + columnNumber: 34, + }; + session.on('Network.requestWillBeSent', common.mustCall(({ params }) => { + assert.strictEqual(params.requestId, 'request-with-user-initiator'); + assert.deepStrictEqual(params.initiator, userInitiator); + })); + inspector.Network.requestWillBeSent({ + requestId: 'request-with-user-initiator', + request: { + url: 'https://nodejs.org/en', + method: 'GET', + headers: {}, + }, + timestamp: 1000, + wallTime: 1000, + initiator: userInitiator, + }); + + // Verify a duplicate requestId is silently ignored. Covers the early + // return when `requests_.contains(request_id)` (network_agent.cc L610). + session.removeAllListeners('Network.requestWillBeSent'); + const duplicateId = 'duplicate-request-id'; + const duplicateParams = { + requestId: duplicateId, + request: { + url: 'https://nodejs.org/en', + method: 'GET', + headers: {}, + }, + timestamp: 1000, + wallTime: 1000, + }; + session.on('Network.requestWillBeSent', common.mustCall(({ params }) => { + assert.strictEqual(params.requestId, duplicateId); + }, 1)); + inspector.Network.requestWillBeSent(duplicateParams); + inspector.Network.requestWillBeSent(duplicateParams); + // Check tht no events are emitted after disabling the domain. await session.post('Network.disable'); session.on('Network.requestWillBeSent', common.mustNotCall()); From f972b0d537965ed59661e1377e1985d382f5fc19 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Tue, 5 May 2026 12:30:29 +0800 Subject: [PATCH 16/18] style: add comment for createInitiator --- lib/internal/inspector/network_http.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/inspector/network_http.js b/lib/internal/inspector/network_http.js index 42ca7c976cd46f..6a388341976a9a 100644 --- a/lib/internal/inspector/network_http.js +++ b/lib/internal/inspector/network_http.js @@ -97,6 +97,7 @@ function createInitiator() { */ function onClientRequestCreated({ request }) { request[kInspectorRequestId] = getNextRequestId(); + // Ensure that the stack obtained here is the one created at the time of actual construction. request[kInitiator] = createInitiator(); } From bf1eeb9783837c233b308fc7ff99cd7a245c3f8f Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Tue, 5 May 2026 14:45:46 +0800 Subject: [PATCH 17/18] inspector: tighten createInitiator coverage --- src/inspector/network_agent.cc | 9 ++++-- .../test-inspector-emit-protocol-event.js | 32 +++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 8f8bd4a73a4aed..19b0c489fb4017 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -86,10 +86,13 @@ static std::unique_ptr V8ToProtocolValue( std::unique_ptr dict = protocol::DictionaryValue::create(); for (uint32_t i = 0; i < property_names->Length(); i++) { - Local key; + // `property_names` is a JSArray returned from GetOwnPropertyNames, so + // indexed access always succeeds. User-defined getters can still throw + // when reading the property value, which is what we guard against. + Local key = + property_names->Get(context, i).ToLocalChecked(); Local property; - if (!property_names->Get(context, i).ToLocal(&key) || - !GetProperty(context, object, key).ToLocal(&property)) { + if (!GetProperty(context, object, key).ToLocal(&property)) { return nullptr; } std::unique_ptr protocol_value = diff --git a/test/parallel/test-inspector-emit-protocol-event.js b/test/parallel/test-inspector-emit-protocol-event.js index a1af972f2353d3..96ec7172f392a0 100644 --- a/test/parallel/test-inspector-emit-protocol-event.js +++ b/test/parallel/test-inspector-emit-protocol-event.js @@ -195,8 +195,8 @@ for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { } // Verify a user-supplied initiator (with stack) is preserved end-to-end. - // Covers the success body of `if (ObjectGetObject(... "stack"))` in - // NetworkAgent::createInitiatorFromObject (network_agent.cc L159). + // Covers the true branch of `if (ObjectGetObject(... "stack"))` in + // NetworkAgent::createInitiatorFromObject. session.removeAllListeners('Network.requestWillBeSent'); const userInitiator = { type: 'script', @@ -221,8 +221,34 @@ for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) { initiator: userInitiator, }); + // Verify a user-supplied initiator without `stack` is forwarded as-is. + // Covers the false branch of `if (ObjectGetObject(... "stack"))` in + // NetworkAgent::createInitiatorFromObject. + session.removeAllListeners('Network.requestWillBeSent'); + const initiatorWithoutStack = { + type: 'script', + url: 'https://nodejs.org/no-stack.js', + lineNumber: 7, + columnNumber: 8, + }; + session.on('Network.requestWillBeSent', common.mustCall(({ params }) => { + assert.strictEqual(params.requestId, 'request-without-initiator-stack'); + assert.deepStrictEqual(params.initiator, initiatorWithoutStack); + })); + inspector.Network.requestWillBeSent({ + requestId: 'request-without-initiator-stack', + request: { + url: 'https://nodejs.org/en', + method: 'GET', + headers: {}, + }, + timestamp: 1000, + wallTime: 1000, + initiator: initiatorWithoutStack, + }); + // Verify a duplicate requestId is silently ignored. Covers the early - // return when `requests_.contains(request_id)` (network_agent.cc L610). + // return when `requests_.contains(request_id)` in NetworkAgent::requestWillBeSent. session.removeAllListeners('Network.requestWillBeSent'); const duplicateId = 'duplicate-request-id'; const duplicateParams = { From 7edffdf3107bd44bf5ff1f9c4490b1cf24f9a1e6 Mon Sep 17 00:00:00 2001 From: GrinZero <774933704@qq.com> Date: Tue, 5 May 2026 14:55:13 +0800 Subject: [PATCH 18/18] style: merge split variable declaration onto one line --- src/inspector/network_agent.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 19b0c489fb4017..77de90843f9ebb 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -89,8 +89,7 @@ static std::unique_ptr V8ToProtocolValue( // `property_names` is a JSArray returned from GetOwnPropertyNames, so // indexed access always succeeds. User-defined getters can still throw // when reading the property value, which is what we guard against. - Local key = - property_names->Get(context, i).ToLocalChecked(); + Local key = property_names->Get(context, i).ToLocalChecked(); Local property; if (!GetProperty(context, object, key).ToLocal(&property)) { return nullptr;