Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions lib/internal/inspector/network_http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -99,6 +101,7 @@ function onClientStreamCreated({ stream, headers }) {
url,
method,
headers: convertedHeaderObject,
hasPostData: !stream.writableEnded,
},
});
}
Expand All @@ -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
Expand All @@ -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,
});
});
}

/**
Expand Down Expand Up @@ -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],
]);
60 changes: 50 additions & 10 deletions test/parallel/test-inspector-network-http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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')) {
Expand All @@ -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');
Expand Down Expand Up @@ -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', () => {});
Expand All @@ -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 ] },
Expand All @@ -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]
Expand Down
Loading