inspector: add http2 tracking support · nodejs/node@772f8f4
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+};