mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
inspector: support inspecting HTTP/2 request and response bodies
Signed-off-by: Darshan Sen <raisinten@gmail.com> PR-URL: https://github.com/nodejs/node/pull/60483 Refs: https://github.com/nodejs/node/issues/53946 Reviewed-By: Ryuhei Shima <shimaryuhei@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
1 parent
76d6be5fc5
commit
c63e88e9a6
2 files changed
+133
-10
No files matched your search
@@ -28,6 +28,8 @@ const {
|
||||
HTTP2_HEADER_STATUS,
|
||||
NGHTTP2_NO_ERROR,
|
||||
} = internalBinding('http2').constants;
|
||||
const EventEmitter = require('events');
|
||||
const { Buffer } = require('buffer');
|
||||
|
||||
const kRequestUrl = Symbol('kRequestUrl');
|
||||
|
||||
@@ -99,6 +101,7 @@ function onClientStreamCreated({ stream, headers }) {
|
||||
url,
|
||||
method,
|
||||
headers: convertedHeaderObject,
|
||||
hasPostData: !stream.writableEnded,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -121,6 +124,66 @@ function onClientStreamError({ stream, 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 {{
|
||||
* stream: import('http2').ClientHttp2Stream,
|
||||
* writev: boolean,
|
||||
* data: Buffer | string | Array<Buffer | {chunk: Buffer|string, encoding: string}>,
|
||||
* encoding: string,
|
||||
* }} event
|
||||
*/
|
||||
function onClientStreamBodyChunkSent({ stream, writev, data, encoding }) {
|
||||
if (typeof stream[kInspectorRequestId] !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk;
|
||||
|
||||
if (writev) {
|
||||
if (data.allBuffers) {
|
||||
chunk = Buffer.concat(data);
|
||||
} else {
|
||||
const buffers = [];
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (typeof data[i].chunk === 'string') {
|
||||
buffers.push(Buffer.from(data[i].chunk, data[i].encoding));
|
||||
} else {
|
||||
buffers.push(data[i].chunk);
|
||||
}
|
||||
}
|
||||
chunk = Buffer.concat(buffers);
|
||||
}
|
||||
} else if (typeof data === 'string') {
|
||||
chunk = Buffer.from(data, encoding);
|
||||
} else {
|
||||
chunk = data;
|
||||
}
|
||||
|
||||
Network.dataSent({
|
||||
requestId: stream[kInspectorRequestId],
|
||||
timestamp: getMonotonicTime(),
|
||||
dataLength: chunk.byteLength,
|
||||
data: chunk,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a request body as fully sent.
|
||||
* @param {{ stream: import('http2').ClientHttp2Stream }} event
|
||||
*/
|
||||
function onClientStreamBodySent({ stream }) {
|
||||
if (typeof stream[kInspectorRequestId] !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
Network.dataSent({
|
||||
requestId: stream[kInspectorRequestId],
|
||||
finished: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When response headers are received, emit Network.responseReceived event.
|
||||
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
|
||||
@@ -146,6 +209,24 @@ function onClientStreamFinish({ stream, headers }) {
|
||||
charset,
|
||||
},
|
||||
});
|
||||
|
||||
// Unlike stream.on('data', ...), this does not put the stream into flowing mode.
|
||||
EventEmitter.prototype.on.call(stream, 'data', (chunk) => {
|
||||
/**
|
||||
* When a chunk of the response body has been received, cache it until `getResponseBody` request
|
||||
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody or
|
||||
* stream it with `streamResourceContent` request.
|
||||
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-streamResourceContent
|
||||
*/
|
||||
|
||||
Network.dataReceived({
|
||||
requestId: stream[kInspectorRequestId],
|
||||
timestamp: getMonotonicTime(),
|
||||
dataLength: chunk.byteLength,
|
||||
encodedDataLength: chunk.byteLength,
|
||||
data: chunk,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,4 +256,6 @@ module.exports = registerDiagnosticChannels([
|
||||
['http2.client.stream.error', onClientStreamError],
|
||||
['http2.client.stream.finish', onClientStreamFinish],
|
||||
['http2.client.stream.close', onClientStreamClose],
|
||||
['http2.client.stream.bodyChunkSent', onClientStreamBodyChunkSent],
|
||||
['http2.client.stream.bodySent', onClientStreamBodySent],
|
||||
]);
|
||||
@@ -14,12 +14,15 @@ const inspector = require('node:inspector/promises');
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
|
||||
const requestBody = { 'hello': 'world' };
|
||||
|
||||
const requestHeaders = {
|
||||
'x-header1': ['value1', 'value2'],
|
||||
[http2.constants.HTTP2_HEADER_ACCEPT_LANGUAGE]: 'en-US',
|
||||
[http2.constants.HTTP2_HEADER_AGE]: 1000,
|
||||
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/json; charset=utf-8',
|
||||
[http2.constants.HTTP2_HEADER_COOKIE]: ['k1=v1', 'k2=v2'],
|
||||
[http2.constants.HTTP2_HEADER_METHOD]: 'GET',
|
||||
[http2.constants.HTTP2_HEADER_METHOD]: 'POST',
|
||||
[http2.constants.HTTP2_HEADER_PATH]: '/hello-world',
|
||||
};
|
||||
|
||||
@@ -54,23 +57,35 @@ const pushResponseHeaders = {
|
||||
[http2.constants.HTTP2_HEADER_STATUS]: 200,
|
||||
};
|
||||
|
||||
const styleCss = 'body { color: red; }\n';
|
||||
const serverResponse = 'hello world\n';
|
||||
|
||||
const kTimeout = 1000;
|
||||
const kDelta = 200;
|
||||
|
||||
const handleStream = (stream, headers) => {
|
||||
const path = headers[http2.constants.HTTP2_HEADER_PATH];
|
||||
let body = '';
|
||||
switch (path) {
|
||||
case '/hello-world':
|
||||
stream.pushStream(pushRequestHeaders, common.mustSucceed((pushStream) => {
|
||||
pushStream.respond(pushResponseHeaders);
|
||||
pushStream.end('body { color: red; }\n');
|
||||
}));
|
||||
stream.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
stream.respond(responseHeaders);
|
||||
stream.on('end', () => {
|
||||
assert.strictEqual(body, JSON.stringify(requestBody));
|
||||
|
||||
setTimeout(() => {
|
||||
stream.end('hello world\n');
|
||||
}, kTimeout);
|
||||
stream.pushStream(pushRequestHeaders, common.mustSucceed((pushStream) => {
|
||||
pushStream.respond(pushResponseHeaders);
|
||||
pushStream.end(styleCss);
|
||||
}));
|
||||
|
||||
stream.respond(responseHeaders);
|
||||
|
||||
setTimeout(() => {
|
||||
stream.end(serverResponse);
|
||||
}, kTimeout);
|
||||
});
|
||||
break;
|
||||
case '/trigger-error':
|
||||
stream.close(http2.constants.NGHTTP2_STREAM_CLOSED);
|
||||
@@ -114,7 +129,6 @@ function verifyRequestWillBeSent({ method, params }, expectedUrl) {
|
||||
|
||||
assert.ok(params.requestId.startsWith('node-network-event-'));
|
||||
assert.strictEqual(params.request.url, expectedUrl);
|
||||
assert.strictEqual(params.request.method, 'GET');
|
||||
assert.strictEqual(typeof params.request.headers, 'object');
|
||||
|
||||
if (expectedUrl.endsWith('/hello-world')) {
|
||||
@@ -123,10 +137,17 @@ function verifyRequestWillBeSent({ method, params }, expectedUrl) {
|
||||
assert.strictEqual(params.request.headers.age, '1000');
|
||||
assert.strictEqual(params.request.headers['x-header1'], 'value1, value2');
|
||||
assert.ok(findFrameInInitiator(__filename, params.initiator));
|
||||
assert.strictEqual(params.request.hasPostData, true);
|
||||
assert.strictEqual(params.request.method, 'POST');
|
||||
} else if (expectedUrl.endsWith('/style.css')) {
|
||||
assert.strictEqual(params.request.headers['x-header3'], 'value1, value2');
|
||||
assert.strictEqual(params.request.headers['x-push'], 'true');
|
||||
assert.ok(!findFrameInInitiator(__filename, params.initiator));
|
||||
assert.strictEqual(params.request.hasPostData, true);
|
||||
assert.strictEqual(params.request.method, 'GET');
|
||||
} else {
|
||||
assert.strictEqual(params.request.hasPostData, false);
|
||||
assert.strictEqual(params.request.method, 'GET');
|
||||
}
|
||||
|
||||
assert.strictEqual(typeof params.timestamp, 'number');
|
||||
@@ -198,6 +219,8 @@ async function testHttp2(secure = false) {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
const request = client.request(requestHeaders);
|
||||
request.write(JSON.stringify(requestBody));
|
||||
request.end();
|
||||
|
||||
// Dump the responses.
|
||||
request.on('data', () => {});
|
||||
@@ -216,6 +239,11 @@ async function testHttp2(secure = false) {
|
||||
verifyRequestWillBeSent(mainRequest, url);
|
||||
verifyRequestWillBeSent(pushRequest, pushedUrl);
|
||||
|
||||
const { postData } = await session.post('Network.getRequestPostData', {
|
||||
requestId: mainRequest.params.requestId
|
||||
});
|
||||
assert.strictEqual(postData, JSON.stringify(requestBody));
|
||||
|
||||
const [
|
||||
{ value: [ mainResponse ] },
|
||||
{ value: [ pushResponse ] },
|
||||
@@ -230,6 +258,18 @@ async function testHttp2(secure = false) {
|
||||
verifyLoadingFinished(event1);
|
||||
verifyLoadingFinished(event2);
|
||||
|
||||
const responseBody = await session.post('Network.getResponseBody', {
|
||||
requestId: mainRequest.params.requestId,
|
||||
});
|
||||
assert.strictEqual(responseBody.base64Encoded, false);
|
||||
assert.strictEqual(responseBody.body, serverResponse);
|
||||
|
||||
const pushResponseBody = await session.post('Network.getResponseBody', {
|
||||
requestId: pushRequest.params.requestId,
|
||||
});
|
||||
assert.strictEqual(pushResponseBody.base64Encoded, true);
|
||||
assert.strictEqual(Buffer.from(pushResponseBody.body, 'base64').toString(), styleCss);
|
||||
|
||||
const mainFinished = [event1, event2]
|
||||
.find((event) => event.params.requestId === mainResponse.params.requestId);
|
||||
const pushFinished = [event1, event2]
|
||||
|
||||
Reference in new issue
Block a user