Skip to content

Commit f185e8a

Browse files
legendecasRafaelGSS
authored andcommitted
inspector: report loadingFinished until the response data is consumed
The `Network.loadingFinished` should be deferred until the response is complete and the data is fully consumed. Also, report correct request url with the specified port by retrieving the host from the request headers. PR-URL: #56372 Refs: #53946 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Kohei Ueno <kohei.ueno119@gmail.com>
1 parent 1e201fd commit f185e8a

6 files changed

+412
-299
lines changed

lib/internal/inspector/network.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const {
4+
NumberMAX_SAFE_INTEGER,
5+
Symbol,
6+
} = primordials;
7+
8+
const { now } = require('internal/perf/utils');
9+
const kInspectorRequestId = Symbol('kInspectorRequestId');
10+
11+
/**
12+
* Return a monotonically increasing time in seconds since an arbitrary point in the past.
13+
* @returns {number}
14+
*/
15+
function getMonotonicTime() {
16+
return now() / 1000;
17+
}
18+
19+
let requestId = 0;
20+
function getNextRequestId() {
21+
if (requestId === NumberMAX_SAFE_INTEGER) {
22+
requestId = 0;
23+
}
24+
return `node-network-event-${++requestId}`;
25+
};
26+
27+
module.exports = {
28+
kInspectorRequestId,
29+
getMonotonicTime,
30+
getNextRequestId,
31+
};
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use strict';
2+
3+
const {
4+
ArrayIsArray,
5+
DateNow,
6+
ObjectEntries,
7+
String,
8+
Symbol,
9+
} = primordials;
10+
11+
const {
12+
kInspectorRequestId,
13+
getMonotonicTime,
14+
getNextRequestId,
15+
} = require('internal/inspector/network');
16+
const dc = require('diagnostics_channel');
17+
const { Network } = require('inspector');
18+
19+
const kResourceType = 'Other';
20+
const kRequestUrl = Symbol('kRequestUrl');
21+
22+
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
23+
const convertHeaderObject = (headers = {}) => {
24+
// The 'host' header that contains the host and port of the URL.
25+
let host;
26+
const dict = {};
27+
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
28+
if (key.toLowerCase() === 'host') {
29+
host = value;
30+
}
31+
if (typeof value === 'string') {
32+
dict[key] = value;
33+
} else if (ArrayIsArray(value)) {
34+
if (key.toLowerCase() === 'cookie') dict[key] = value.join('; ');
35+
// ChromeDevTools frontend treats 'set-cookie' as a special case
36+
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
37+
else if (key.toLowerCase() === 'set-cookie') dict[key] = value.join('\n');
38+
else dict[key] = value.join(', ');
39+
} else {
40+
dict[key] = String(value);
41+
}
42+
}
43+
return [host, dict];
44+
};
45+
46+
/**
47+
* When a client request starts, emit Network.requestWillBeSent event.
48+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent
49+
* @param {{ request: import('http').ClientRequest }} event
50+
*/
51+
function onClientRequestStart({ request }) {
52+
request[kInspectorRequestId] = getNextRequestId();
53+
54+
const { 0: host, 1: headers } = convertHeaderObject(request.getHeaders());
55+
const url = `${request.protocol}//${host}${request.path}`;
56+
request[kRequestUrl] = url;
57+
58+
Network.requestWillBeSent({
59+
requestId: request[kInspectorRequestId],
60+
timestamp: getMonotonicTime(),
61+
wallTime: DateNow(),
62+
request: {
63+
url,
64+
method: request.method,
65+
headers,
66+
},
67+
});
68+
}
69+
70+
/**
71+
* When a client request errors, emit Network.loadingFailed event.
72+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed
73+
* @param {{ request: import('http').ClientRequest, error: any }} event
74+
*/
75+
function onClientRequestError({ request, error }) {
76+
if (typeof request[kInspectorRequestId] !== 'string') {
77+
return;
78+
}
79+
Network.loadingFailed({
80+
requestId: request[kInspectorRequestId],
81+
timestamp: getMonotonicTime(),
82+
type: kResourceType,
83+
errorText: error.message,
84+
});
85+
}
86+
87+
/**
88+
* When response headers are received, emit Network.responseReceived event.
89+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
90+
* @param {{ request: import('http').ClientRequest, error: any }} event
91+
*/
92+
function onClientResponseFinish({ request, response }) {
93+
if (typeof request[kInspectorRequestId] !== 'string') {
94+
return;
95+
}
96+
Network.responseReceived({
97+
requestId: request[kInspectorRequestId],
98+
timestamp: getMonotonicTime(),
99+
type: kResourceType,
100+
response: {
101+
url: request[kRequestUrl],
102+
status: response.statusCode,
103+
statusText: response.statusMessage ?? '',
104+
headers: convertHeaderObject(response.headers)[1],
105+
},
106+
});
107+
108+
// Wait until the response body is consumed by user code.
109+
response.once('end', () => {
110+
Network.loadingFinished({
111+
requestId: request[kInspectorRequestId],
112+
timestamp: getMonotonicTime(),
113+
});
114+
});
115+
}
116+
117+
function enable() {
118+
dc.subscribe('http.client.request.start', onClientRequestStart);
119+
dc.subscribe('http.client.request.error', onClientRequestError);
120+
dc.subscribe('http.client.response.finish', onClientResponseFinish);
121+
}
122+
123+
function disable() {
124+
dc.unsubscribe('http.client.request.start', onClientRequestStart);
125+
dc.unsubscribe('http.client.request.error', onClientRequestError);
126+
dc.unsubscribe('http.client.response.finish', onClientResponseFinish);
127+
}
128+
129+
module.exports = {
130+
enable,
131+
disable,
132+
};

lib/internal/inspector_network_tracking.js

+6-93
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,15 @@
11
'use strict';
22

3-
const {
4-
ArrayIsArray,
5-
DateNow,
6-
ObjectEntries,
7-
String,
8-
} = primordials;
9-
10-
let dc;
11-
let Network;
12-
13-
let requestId = 0;
14-
const getNextRequestId = () => `node-network-event-${++requestId}`;
15-
16-
// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
17-
const headerObjectToDictionary = (headers = {}) => {
18-
const dict = {};
19-
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
20-
if (typeof value === 'string') {
21-
dict[key] = value;
22-
} else if (ArrayIsArray(value)) {
23-
if (key.toLowerCase() === 'cookie') dict[key] = value.join('; ');
24-
// ChromeDevTools frontend treats 'set-cookie' as a special case
25-
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
26-
else if (key.toLowerCase() === 'set-cookie') dict[key] = value.join('\n');
27-
else dict[key] = value.join(', ');
28-
} else {
29-
dict[key] = String(value);
30-
}
31-
}
32-
return dict;
33-
};
34-
35-
function onClientRequestStart({ request }) {
36-
const url = `${request.protocol}//${request.host}${request.path}`;
37-
const wallTime = DateNow();
38-
const timestamp = wallTime / 1000;
39-
request._inspectorRequestId = getNextRequestId();
40-
Network.requestWillBeSent({
41-
requestId: request._inspectorRequestId,
42-
timestamp,
43-
wallTime,
44-
request: {
45-
url,
46-
method: request.method,
47-
headers: headerObjectToDictionary(request.getHeaders()),
48-
},
49-
});
50-
}
51-
52-
function onClientRequestError({ request, error }) {
53-
if (typeof request._inspectorRequestId !== 'string') {
54-
return;
55-
}
56-
const timestamp = DateNow() / 1000;
57-
Network.loadingFailed({
58-
requestId: request._inspectorRequestId,
59-
timestamp,
60-
type: 'Other',
61-
errorText: error.message,
62-
});
63-
}
64-
65-
function onClientResponseFinish({ request, response }) {
66-
if (typeof request._inspectorRequestId !== 'string') {
67-
return;
68-
}
69-
const url = `${request.protocol}//${request.host}${request.path}`;
70-
const timestamp = DateNow() / 1000;
71-
Network.responseReceived({
72-
requestId: request._inspectorRequestId,
73-
timestamp,
74-
type: 'Other',
75-
response: {
76-
url,
77-
status: response.statusCode,
78-
statusText: response.statusMessage ?? '',
79-
headers: headerObjectToDictionary(response.headers),
80-
},
81-
});
82-
Network.loadingFinished({
83-
requestId: request._inspectorRequestId,
84-
timestamp,
85-
});
86-
}
87-
883
function enable() {
89-
dc ??= require('diagnostics_channel');
90-
Network ??= require('inspector').Network;
91-
dc.subscribe('http.client.request.start', onClientRequestStart);
92-
dc.subscribe('http.client.request.error', onClientRequestError);
93-
dc.subscribe('http.client.response.finish', onClientResponseFinish);
4+
require('internal/inspector/network_http').enable();
5+
// TODO: add undici request/websocket tracking.
6+
// https://github.com/nodejs/node/issues/53946
947
}
958

969
function disable() {
97-
dc.unsubscribe('http.client.request.start', onClientRequestStart);
98-
dc.unsubscribe('http.client.request.error', onClientRequestError);
99-
dc.unsubscribe('http.client.response.finish', onClientResponseFinish);
10+
require('internal/inspector/network_http').disable();
11+
// TODO: add undici request/websocket tracking.
12+
// https://github.com/nodejs/node/issues/53946
10013
}
10114

10215
module.exports = {

src/node_builtins.cc

+2
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
119119
builtin_categories.cannot_be_required = std::set<std::string> {
120120
#if !HAVE_INSPECTOR
121121
"inspector", "inspector/promises", "internal/util/inspector",
122+
"internal/inspector/network", "internal/inspector/network_http",
123+
"internal/inspector_async_hook", "internal/inspector_network_tracking",
122124
#endif // !HAVE_INSPECTOR
123125

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

0 commit comments

Comments
 (0)