Skip to content

Commit f67af83

Browse files
committed
inspector: support inspecting HTTP/2 request and response bodies
Signed-off-by: Darshan Sen <raisinten@gmail.com>
1 parent f46152f commit f67af83

File tree

2 files changed

+133
-10
lines changed

2 files changed

+133
-10
lines changed

lib/internal/inspector/network_http2.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const {
2828
HTTP2_HEADER_STATUS,
2929
NGHTTP2_NO_ERROR,
3030
} = internalBinding('http2').constants;
31+
const EventEmitter = require('events');
32+
const { Buffer } = require('buffer');
3133

3234
const kRequestUrl = Symbol('kRequestUrl');
3335

@@ -99,6 +101,7 @@ function onClientStreamCreated({ stream, headers }) {
99101
url,
100102
method,
101103
headers: convertedHeaderObject,
104+
hasPostData: !stream.writableEnded,
102105
},
103106
});
104107
}
@@ -121,6 +124,66 @@ function onClientStreamError({ stream, error }) {
121124
});
122125
}
123126

127+
/**
128+
* When a chunk of the request body is being sent, cache it until `getRequestPostData` request.
129+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getRequestPostData
130+
* @param {{
131+
* stream: import('http2').ClientHttp2Stream,
132+
* writev: boolean,
133+
* data: Buffer | string | Array<Buffer | {chunk: Buffer|string, encoding: string}>,
134+
* encoding: string,
135+
* }} event
136+
*/
137+
function onClientStreamBodyChunkSent({ stream, writev, data, encoding }) {
138+
if (typeof stream[kInspectorRequestId] !== 'string') {
139+
return;
140+
}
141+
142+
let chunk;
143+
144+
if (writev) {
145+
if (data.allBuffers) {
146+
chunk = Buffer.concat(data);
147+
} else {
148+
const buffers = [];
149+
for (let i = 0; i < data.length; ++i) {
150+
if (typeof data[i].chunk === 'string') {
151+
buffers.push(Buffer.from(data[i].chunk, data[i].encoding));
152+
} else {
153+
buffers.push(data[i].chunk);
154+
}
155+
}
156+
chunk = Buffer.concat(buffers);
157+
}
158+
} else if (typeof data === 'string') {
159+
chunk = Buffer.from(data, encoding);
160+
} else {
161+
chunk = data;
162+
}
163+
164+
Network.dataSent({
165+
requestId: stream[kInspectorRequestId],
166+
timestamp: getMonotonicTime(),
167+
dataLength: chunk.byteLength,
168+
data: chunk,
169+
});
170+
}
171+
172+
/**
173+
* Mark a request body as fully sent.
174+
* @param {{ stream: import('http2').ClientHttp2Stream }} event
175+
*/
176+
function onClientStreamBodySent({ stream }) {
177+
if (typeof stream[kInspectorRequestId] !== 'string') {
178+
return;
179+
}
180+
181+
Network.dataSent({
182+
requestId: stream[kInspectorRequestId],
183+
finished: true,
184+
});
185+
}
186+
124187
/**
125188
* When response headers are received, emit Network.responseReceived event.
126189
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
@@ -146,6 +209,24 @@ function onClientStreamFinish({ stream, headers }) {
146209
charset,
147210
},
148211
});
212+
213+
// Unlike stream.on('data', ...), this does not put the stream into flowing mode.
214+
EventEmitter.prototype.on.call(stream, 'data', (chunk) => {
215+
/**
216+
* When a chunk of the response body has been received, cache it until `getResponseBody` request
217+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody or
218+
* stream it with `streamResourceContent` request.
219+
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-streamResourceContent
220+
*/
221+
222+
Network.dataReceived({
223+
requestId: stream[kInspectorRequestId],
224+
timestamp: getMonotonicTime(),
225+
dataLength: chunk.byteLength,
226+
encodedDataLength: chunk.byteLength,
227+
data: chunk,
228+
});
229+
});
149230
}
150231

151232
/**
@@ -175,4 +256,6 @@ module.exports = registerDiagnosticChannels([
175256
['http2.client.stream.error', onClientStreamError],
176257
['http2.client.stream.finish', onClientStreamFinish],
177258
['http2.client.stream.close', onClientStreamClose],
259+
['http2.client.stream.bodyChunkSent', onClientStreamBodyChunkSent],
260+
['http2.client.stream.bodySent', onClientStreamBodySent],
178261
]);

test/parallel/test-inspector-network-http2.js

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ const inspector = require('node:inspector/promises');
1414
const session = new inspector.Session();
1515
session.connect();
1616

17+
const requestBody = { 'hello': 'world' };
18+
1719
const requestHeaders = {
1820
'x-header1': ['value1', 'value2'],
1921
[http2.constants.HTTP2_HEADER_ACCEPT_LANGUAGE]: 'en-US',
2022
[http2.constants.HTTP2_HEADER_AGE]: 1000,
23+
[http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/json; charset=utf-8',
2124
[http2.constants.HTTP2_HEADER_COOKIE]: ['k1=v1', 'k2=v2'],
22-
[http2.constants.HTTP2_HEADER_METHOD]: 'GET',
25+
[http2.constants.HTTP2_HEADER_METHOD]: 'POST',
2326
[http2.constants.HTTP2_HEADER_PATH]: '/hello-world',
2427
};
2528

@@ -54,23 +57,35 @@ const pushResponseHeaders = {
5457
[http2.constants.HTTP2_HEADER_STATUS]: 200,
5558
};
5659

60+
const styleCss = 'body { color: red; }\n';
61+
const serverResponse = 'hello world\n';
62+
5763
const kTimeout = 1000;
5864
const kDelta = 200;
5965

6066
const handleStream = (stream, headers) => {
6167
const path = headers[http2.constants.HTTP2_HEADER_PATH];
68+
let body = '';
6269
switch (path) {
6370
case '/hello-world':
64-
stream.pushStream(pushRequestHeaders, common.mustSucceed((pushStream) => {
65-
pushStream.respond(pushResponseHeaders);
66-
pushStream.end('body { color: red; }\n');
67-
}));
71+
stream.on('data', (chunk) => {
72+
body += chunk;
73+
});
6874

69-
stream.respond(responseHeaders);
75+
stream.on('end', () => {
76+
assert.strictEqual(body, JSON.stringify(requestBody));
7077

71-
setTimeout(() => {
72-
stream.end('hello world\n');
73-
}, kTimeout);
78+
stream.pushStream(pushRequestHeaders, common.mustSucceed((pushStream) => {
79+
pushStream.respond(pushResponseHeaders);
80+
pushStream.end(styleCss);
81+
}));
82+
83+
stream.respond(responseHeaders);
84+
85+
setTimeout(() => {
86+
stream.end(serverResponse);
87+
}, kTimeout);
88+
});
7489
break;
7590
case '/trigger-error':
7691
stream.close(http2.constants.NGHTTP2_STREAM_CLOSED);
@@ -114,7 +129,6 @@ function verifyRequestWillBeSent({ method, params }, expectedUrl) {
114129

115130
assert.ok(params.requestId.startsWith('node-network-event-'));
116131
assert.strictEqual(params.request.url, expectedUrl);
117-
assert.strictEqual(params.request.method, 'GET');
118132
assert.strictEqual(typeof params.request.headers, 'object');
119133

120134
if (expectedUrl.endsWith('/hello-world')) {
@@ -123,10 +137,17 @@ function verifyRequestWillBeSent({ method, params }, expectedUrl) {
123137
assert.strictEqual(params.request.headers.age, '1000');
124138
assert.strictEqual(params.request.headers['x-header1'], 'value1, value2');
125139
assert.ok(findFrameInInitiator(__filename, params.initiator));
140+
assert.strictEqual(params.request.hasPostData, true);
141+
assert.strictEqual(params.request.method, 'POST');
126142
} else if (expectedUrl.endsWith('/style.css')) {
127143
assert.strictEqual(params.request.headers['x-header3'], 'value1, value2');
128144
assert.strictEqual(params.request.headers['x-push'], 'true');
129145
assert.ok(!findFrameInInitiator(__filename, params.initiator));
146+
assert.strictEqual(params.request.hasPostData, true);
147+
assert.strictEqual(params.request.method, 'GET');
148+
} else {
149+
assert.strictEqual(params.request.hasPostData, false);
150+
assert.strictEqual(params.request.method, 'GET');
130151
}
131152

132153
assert.strictEqual(typeof params.timestamp, 'number');
@@ -198,6 +219,8 @@ async function testHttp2(secure = false) {
198219
rejectUnauthorized: false,
199220
});
200221
const request = client.request(requestHeaders);
222+
request.write(JSON.stringify(requestBody));
223+
request.end();
201224

202225
// Dump the responses.
203226
request.on('data', () => {});
@@ -216,6 +239,11 @@ async function testHttp2(secure = false) {
216239
verifyRequestWillBeSent(mainRequest, url);
217240
verifyRequestWillBeSent(pushRequest, pushedUrl);
218241

242+
const { postData } = await session.post('Network.getRequestPostData', {
243+
requestId: mainRequest.params.requestId
244+
});
245+
assert.strictEqual(postData, JSON.stringify(requestBody));
246+
219247
const [
220248
{ value: [ mainResponse ] },
221249
{ value: [ pushResponse ] },
@@ -230,6 +258,18 @@ async function testHttp2(secure = false) {
230258
verifyLoadingFinished(event1);
231259
verifyLoadingFinished(event2);
232260

261+
const responseBody = await session.post('Network.getResponseBody', {
262+
requestId: mainRequest.params.requestId,
263+
});
264+
assert.strictEqual(responseBody.base64Encoded, false);
265+
assert.strictEqual(responseBody.body, serverResponse);
266+
267+
const pushResponseBody = await session.post('Network.getResponseBody', {
268+
requestId: pushRequest.params.requestId,
269+
});
270+
assert.strictEqual(pushResponseBody.base64Encoded, true);
271+
assert.strictEqual(Buffer.from(pushResponseBody.body, 'base64').toString(), styleCss);
272+
233273
const mainFinished = [event1, event2]
234274
.find((event) => event.params.requestId === mainResponse.params.requestId);
235275
const pushFinished = [event1, event2]

0 commit comments

Comments
 (0)