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+

};