Skip to content

Commit b806661

Browse files
RaisinTenrichardlau
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 fd7559f commit b806661

4 files changed

Lines changed: 496 additions & 2 deletions

File tree

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+
constdc=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+
constkRequestUrl=Symbol('kRequestUrl');
33+
34+
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
35+
functionconvertHeaderObject(headers={}){
36+
letscheme;
37+
letauthority;
38+
letpath;
39+
letmethod;
40+
letstatusCode;
41+
letcharset;
42+
letmimeType;
43+
constdict={};
44+
45+
for(const{0: key,1: value}ofObjectEntries(headers)){
46+
constlowerCasedKey=key.toLowerCase();
47+
48+
if(lowerCasedKey===HTTP2_HEADER_SCHEME){
49+
scheme=value;
50+
}elseif(lowerCasedKey===HTTP2_HEADER_AUTHORITY){
51+
authority=value;
52+
}elseif(lowerCasedKey===HTTP2_HEADER_PATH){
53+
path=value;
54+
}elseif(lowerCasedKey===HTTP2_HEADER_METHOD){
55+
method=value;
56+
}elseif(lowerCasedKey===HTTP2_HEADER_STATUS){
57+
statusCode=value;
58+
}elseif(lowerCasedKey===HTTP2_HEADER_CONTENT_TYPE){
59+
constresult=sniffMimeType(value);
60+
charset=result.charset;
61+
mimeType=result.mimeType;
62+
}
63+
64+
if(typeofvalue==='string'){
65+
dict[key]=value;
66+
}elseif(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+
elseif(lowerCasedKey===HTTP2_HEADER_SET_COOKIE)dict[key]=value.join('\n');
71+
elsedict[key]=value.join(', ');
72+
}else{
73+
dict[key]=String(value);
74+
}
75+
}
76+
77+
consturl=`${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+
functiononClientStreamCreated({ 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+
functiononClientStreamError({ stream, error }){
112+
if(typeofstream[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+
functiononClientStreamFinish({ stream, headers }){
130+
if(typeofstream[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+
functiononClientStreamClose({ stream }){
157+
if(typeofstream[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+
functionenable(){
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+
functiondisable(){
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
functionenable(){
44
require('internal/inspector/network_http').enable();
5+
require('internal/inspector/network_http2').enable();
56
require('internal/inspector/network_undici').enable();
67
}
78

89
functiondisable(){
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
@@ -123,8 +123,8 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
123123
#if !HAVE_INSPECTOR
124124
"inspector", "inspector/promises", "internal/util/inspector",
125125
"internal/inspector/network", "internal/inspector/network_http",
126-
"internal/inspector/network_undici", "internal/inspector_async_hook",
127-
"internal/inspector_network_tracking",
126+
"internal/inspector/network_http2", "internal/inspector/network_undici",
127+
"internal/inspector_async_hook", "internal/inspector_network_tracking",
128128
#endif// !HAVE_INSPECTOR
129129

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

0 commit comments

Comments
 (0)