Skip to content

Commit 772f8f4

Browse files
RaisinTentargos
authored andcommitted
inspector: add http2 tracking support
This allows tracking HTTP/2 calls through the Network tab of Chrome DevTools for Node.js. Signed-off-by: Darshan Sen <raisinten@gmail.com> PR-URL: #59611 Refs: #53946 Reviewed-By: Ryuhei Shima <shimaryuhei@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Kohei Ueno <kohei.ueno119@gmail.com>
1 parent 0379a8b commit 772f8f4

File tree

4 files changed

+496
-2
lines changed

4 files changed

+496
-2
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
'use strict';
2+
3+
const {
4+
ArrayIsArray,
5+
DateNow,
6+
ObjectEntries,
7+
String,
8+
Symbol,
9+
} = primordials;
10+
11+
const {
12+
kInspectorRequestId,
13+
kResourceType,
14+
getMonotonicTime,
15+
getNextRequestId,
16+
sniffMimeType,
17+
} = require('internal/inspector/network');
18+
const dc = require('diagnostics_channel');
19+
const { Network } = require('inspector');
20+
const {
21+
HTTP2_HEADER_AUTHORITY,
22+
HTTP2_HEADER_CONTENT_TYPE,
23+
HTTP2_HEADER_COOKIE,
24+
HTTP2_HEADER_METHOD,
25+
HTTP2_HEADER_PATH,
26+
HTTP2_HEADER_SCHEME,
27+
HTTP2_HEADER_SET_COOKIE,
28+
HTTP2_HEADER_STATUS,
29+
NGHTTP2_NO_ERROR,
30+
} = internalBinding('http2').constants;
31+
32+
const kRequestUrl = Symbol('kRequestUrl');
33+
34+
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
35+
function convertHeaderObject(headers = {}) {
36+
let scheme;
37+
let authority;
38+
let path;
39+
let method;
40+
let statusCode;
41+
let charset;
42+
let mimeType;
43+
const dict = {};
44+
45+
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
46+
const lowerCasedKey = key.toLowerCase();
47+
48+
if (lowerCasedKey === HTTP2_HEADER_SCHEME) {
49+
scheme = value;
50+
} else if (lowerCasedKey === HTTP2_HEADER_AUTHORITY) {
51+
authority = value;
52+
} else if (lowerCasedKey === HTTP2_HEADER_PATH) {
53+
path = value;
54+
} else if (lowerCasedKey === HTTP2_HEADER_METHOD) {
55+
method = value;
56+
} else if (lowerCasedKey === HTTP2_HEADER_STATUS) {
57+
statusCode = value;
58+
} else if (lowerCasedKey === HTTP2_HEADER_CONTENT_TYPE) {
59+
const result = sniffMimeType(value);
60+
charset = result.charset;
61+
mimeType = result.mimeType;
62+
}
63+
64+
if (typeof value === 'string') {
65+
dict[key] = value;
66+
} else if (ArrayIsArray(value)) {
67+
if (lowerCasedKey === HTTP2_HEADER_COOKIE) dict[key] = value.join('; ');
68+
// ChromeDevTools frontend treats 'set-cookie' as a special case
69+
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
70+
else if (lowerCasedKey === HTTP2_HEADER_SET_COOKIE) dict[key] = value.join('\n');
71+
else dict[key] = value.join(', ');
72+
} else {
73+
dict[key] = String(value);
74+
}
75+
}
76+
77+
const url = `${scheme}://${authority}${path}`;
78+
79+
return [dict, url, method, statusCode, charset, mimeType];
80+
}
81+
82+
/**
83+
* When a client stream is created, emit Network.requestWillBeSent event.
84+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent
85+
* @param {{ stream: import('http2').ClientHttp2Stream, headers: object }} event
86+
*/
87+
function onClientStreamCreated({ stream, headers }) {
88+
stream[kInspectorRequestId] = getNextRequestId();
89+
90+
const { 0: convertedHeaderObject, 1: url, 2: method, 4: charset } = convertHeaderObject(headers);
91+
stream[kRequestUrl] = url;
92+
93+
Network.requestWillBeSent({
94+
requestId: stream[kInspectorRequestId],
95+
timestamp: getMonotonicTime(),
96+
wallTime: DateNow(),
97+
charset,
98+
request: {
99+
url,
100+
method,
101+
headers: convertedHeaderObject,
102+
},
103+
});
104+
}
105+
106+
/**
107+
* When a client stream errors, emit Network.loadingFailed event.
108+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed
109+
* @param {{ stream: import('http2').ClientHttp2Stream, error: any }} event
110+
*/
111+
function onClientStreamError({ stream, error }) {
112+
if (typeof stream[kInspectorRequestId] !== 'string') {
113+
return;
114+
}
115+
116+
Network.loadingFailed({
117+
requestId: stream[kInspectorRequestId],
118+
timestamp: getMonotonicTime(),
119+
type: kResourceType.Other,
120+
errorText: error.message,
121+
});
122+
}
123+
124+
/**
125+
* When response headers are received, emit Network.responseReceived event.
126+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
127+
* @param {{ stream: import('http2').ClientHttp2Stream, headers: object }} event
128+
*/
129+
function onClientStreamFinish({ stream, headers }) {
130+
if (typeof stream[kInspectorRequestId] !== 'string') {
131+
return;
132+
}
133+
134+
const { 0: convertedHeaderObject, 3: statusCode, 4: charset, 5: mimeType } = convertHeaderObject(headers);
135+
136+
Network.responseReceived({
137+
requestId: stream[kInspectorRequestId],
138+
timestamp: getMonotonicTime(),
139+
type: kResourceType.Other,
140+
response: {
141+
url: stream[kRequestUrl],
142+
status: statusCode,
143+
statusText: '',
144+
headers: convertedHeaderObject,
145+
mimeType,
146+
charset,
147+
},
148+
});
149+
}
150+
151+
/**
152+
* When user code completes consuming the response body, emit Network.loadingFinished event.
153+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFinished
154+
* @param {{ stream: import('http2').ClientHttp2Stream }} event
155+
*/
156+
function onClientStreamClose({ stream }) {
157+
if (typeof stream[kInspectorRequestId] !== 'string') {
158+
return;
159+
}
160+
161+
if (stream.rstCode !== NGHTTP2_NO_ERROR) {
162+
// This is an error case, so only Network.loadingFailed should be emitted
163+
// which is already done by onClientStreamError().
164+
return;
165+
}
166+
167+
Network.loadingFinished({
168+
requestId: stream[kInspectorRequestId],
169+
timestamp: getMonotonicTime(),
170+
});
171+
}
172+
173+
function enable() {
174+
dc.subscribe('http2.client.stream.created', onClientStreamCreated);
175+
dc.subscribe('http2.client.stream.error', onClientStreamError);
176+
dc.subscribe('http2.client.stream.finish', onClientStreamFinish);
177+
dc.subscribe('http2.client.stream.close', onClientStreamClose);
178+
}
179+
180+
function disable() {
181+
dc.unsubscribe('http2.client.stream.created', onClientStreamCreated);
182+
dc.unsubscribe('http2.client.stream.error', onClientStreamError);
183+
dc.unsubscribe('http2.client.stream.finish', onClientStreamFinish);
184+
dc.unsubscribe('http2.client.stream.close', onClientStreamClose);
185+
}
186+
187+
module.exports = {
188+
enable,
189+
disable,
190+
};

lib/internal/inspector_network_tracking.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
function enable() {
44
require('internal/inspector/network_http').enable();
5+
require('internal/inspector/network_http2').enable();
56
require('internal/inspector/network_undici').enable();
67
}
78

89
function disable() {
910
require('internal/inspector/network_http').disable();
11+
require('internal/inspector/network_http2').disable();
1012
require('internal/inspector/network_undici').disable();
1113
}
1214

src/node_builtins.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
122122
#if !HAVE_INSPECTOR
123123
"inspector", "inspector/promises", "internal/util/inspector",
124124
"internal/inspector/network", "internal/inspector/network_http",
125-
"internal/inspector/network_undici", "internal/inspector_async_hook",
126-
"internal/inspector_network_tracking",
125+
"internal/inspector/network_http2", "internal/inspector/network_undici",
126+
"internal/inspector_async_hook", "internal/inspector_network_tracking",
127127
#endif // !HAVE_INSPECTOR
128128

129129
#if !NODE_USE_V8_PLATFORM || !defined(NODE_HAVE_I18N_SUPPORT)

0 commit comments

Comments
 (0)