Skip to content

Commit f185e8a

Browse files
legendecasRafaelGSS
authored andcommittedJan 21, 2025
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 <[email protected]> Reviewed-By: Kohei Ueno <[email protected]>
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)

‎test/parallel/test-inspector-network-domain.js

-206
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Flags: --inspect=0 --experimental-network-inspection
2+
'use strict';
3+
const common = require('../common');
4+
5+
common.skipIfInspectorDisabled();
6+
7+
const assert = require('node:assert');
8+
const { once } = require('node:events');
9+
const { addresses } = require('../common/internet');
10+
const fixtures = require('../common/fixtures');
11+
const http = require('node:http');
12+
const https = require('node:https');
13+
const inspector = require('node:inspector/promises');
14+
15+
const session = new inspector.Session();
16+
session.connect();
17+
18+
const requestHeaders = {
19+
'accept-language': 'en-US',
20+
'Cookie': ['k1=v1', 'k2=v2'],
21+
'age': 1000,
22+
'x-header1': ['value1', 'value2']
23+
};
24+
25+
const setResponseHeaders = (res) => {
26+
res.setHeader('server', 'node');
27+
res.setHeader('etag', 12345);
28+
res.setHeader('Set-Cookie', ['key1=value1', 'key2=value2']);
29+
res.setHeader('x-header2', ['value1', 'value2']);
30+
};
31+
32+
const kTimeout = 1000;
33+
const kDelta = 200;
34+
35+
const handleRequest = (req, res) => {
36+
const path = req.url;
37+
switch (path) {
38+
case '/hello-world':
39+
setResponseHeaders(res);
40+
res.writeHead(200);
41+
// Ensure the header is sent.
42+
res.write('\n');
43+
44+
setTimeout(() => {
45+
res.end('hello world\n');
46+
}, kTimeout);
47+
break;
48+
default:
49+
assert(false, `Unexpected path: ${path}`);
50+
}
51+
};
52+
53+
const httpServer = http.createServer(handleRequest);
54+
55+
const httpsServer = https.createServer({
56+
key: fixtures.readKey('agent1-key.pem'),
57+
cert: fixtures.readKey('agent1-cert.pem')
58+
}, handleRequest);
59+
60+
const terminate = () => {
61+
session.disconnect();
62+
httpServer.close();
63+
httpsServer.close();
64+
inspector.close();
65+
};
66+
67+
function verifyRequestWillBeSent({ method, params }, expect) {
68+
assert.strictEqual(method, 'Network.requestWillBeSent');
69+
70+
assert.ok(params.requestId.startsWith('node-network-event-'));
71+
assert.strictEqual(params.request.url, expect.url);
72+
assert.strictEqual(params.request.method, 'GET');
73+
assert.strictEqual(typeof params.request.headers, 'object');
74+
assert.strictEqual(params.request.headers['accept-language'], 'en-US');
75+
assert.strictEqual(params.request.headers.cookie, 'k1=v1; k2=v2');
76+
assert.strictEqual(params.request.headers.age, '1000');
77+
assert.strictEqual(params.request.headers['x-header1'], 'value1, value2');
78+
assert.strictEqual(typeof params.timestamp, 'number');
79+
assert.strictEqual(typeof params.wallTime, 'number');
80+
81+
return params;
82+
}
83+
84+
function verifyResponseReceived({ method, params }, expect) {
85+
assert.strictEqual(method, 'Network.responseReceived');
86+
87+
assert.ok(params.requestId.startsWith('node-network-event-'));
88+
assert.strictEqual(typeof params.timestamp, 'number');
89+
assert.strictEqual(params.type, 'Other');
90+
assert.strictEqual(params.response.status, 200);
91+
assert.strictEqual(params.response.statusText, 'OK');
92+
assert.strictEqual(params.response.url, expect.url);
93+
assert.strictEqual(typeof params.response.headers, 'object');
94+
assert.strictEqual(params.response.headers.server, 'node');
95+
assert.strictEqual(params.response.headers.etag, '12345');
96+
assert.strictEqual(params.response.headers['set-cookie'], 'key1=value1\nkey2=value2');
97+
assert.strictEqual(params.response.headers['x-header2'], 'value1, value2');
98+
99+
return params;
100+
}
101+
102+
function verifyLoadingFinished({ method, params }) {
103+
assert.strictEqual(method, 'Network.loadingFinished');
104+
105+
assert.ok(params.requestId.startsWith('node-network-event-'));
106+
assert.strictEqual(typeof params.timestamp, 'number');
107+
return params;
108+
}
109+
110+
function verifyLoadingFailed({ method, params }) {
111+
assert.strictEqual(method, 'Network.loadingFailed');
112+
113+
assert.ok(params.requestId.startsWith('node-network-event-'));
114+
assert.strictEqual(typeof params.timestamp, 'number');
115+
assert.strictEqual(params.type, 'Other');
116+
assert.strictEqual(typeof params.errorText, 'string');
117+
}
118+
119+
async function testHttpGet() {
120+
const url = `http://127.0.0.1:${httpServer.address().port}/hello-world`;
121+
const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent')
122+
.then(([event]) => verifyRequestWillBeSent(event, { url }));
123+
124+
const responseReceivedFuture = once(session, 'Network.responseReceived')
125+
.then(([event]) => verifyResponseReceived(event, { url }));
126+
127+
const loadingFinishedFuture = once(session, 'Network.loadingFinished')
128+
.then(([event]) => verifyLoadingFinished(event));
129+
130+
http.get({
131+
host: '127.0.0.1',
132+
port: httpServer.address().port,
133+
path: '/hello-world',
134+
headers: requestHeaders
135+
}, common.mustCall((res) => {
136+
// Dump the response.
137+
res.on('data', () => {});
138+
res.on('end', () => {});
139+
}));
140+
141+
await requestWillBeSentFuture;
142+
const responseReceived = await responseReceivedFuture;
143+
const loadingFinished = await loadingFinishedFuture;
144+
145+
const delta = (loadingFinished.timestamp - responseReceived.timestamp) * 1000;
146+
assert.ok(delta > kDelta);
147+
}
148+
149+
async function testHttpsGet() {
150+
const url = `https://127.0.0.1:${httpsServer.address().port}/hello-world`;
151+
const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent')
152+
.then(([event]) => verifyRequestWillBeSent(event, { url }));
153+
154+
const responseReceivedFuture = once(session, 'Network.responseReceived')
155+
.then(([event]) => verifyResponseReceived(event, { url }));
156+
157+
const loadingFinishedFuture = once(session, 'Network.loadingFinished')
158+
.then(([event]) => verifyLoadingFinished(event));
159+
160+
https.get({
161+
host: '127.0.0.1',
162+
port: httpsServer.address().port,
163+
path: '/hello-world',
164+
rejectUnauthorized: false,
165+
headers: requestHeaders,
166+
}, common.mustCall((res) => {
167+
// Dump the response.
168+
res.on('data', () => {});
169+
res.on('end', () => {});
170+
}));
171+
172+
await requestWillBeSentFuture;
173+
const responseReceived = await responseReceivedFuture;
174+
const loadingFinished = await loadingFinishedFuture;
175+
176+
const delta = (loadingFinished.timestamp - responseReceived.timestamp) * 1000;
177+
assert.ok(delta > kDelta);
178+
}
179+
180+
async function testHttpError() {
181+
const url = `http://${addresses.INVALID_HOST}/`;
182+
const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent')
183+
.then(([event]) => verifyRequestWillBeSent(event, { url }));
184+
session.on('Network.responseReceived', common.mustNotCall());
185+
session.on('Network.loadingFinished', common.mustNotCall());
186+
187+
const loadingFailedFuture = once(session, 'Network.loadingFailed')
188+
.then(([event]) => verifyLoadingFailed(event));
189+
190+
http.get({
191+
host: addresses.INVALID_HOST,
192+
headers: requestHeaders,
193+
}, common.mustNotCall()).on('error', common.mustCall());
194+
195+
await requestWillBeSentFuture;
196+
await loadingFailedFuture;
197+
}
198+
199+
async function testHttpsError() {
200+
const url = `https://${addresses.INVALID_HOST}/`;
201+
const requestWillBeSentFuture = once(session, 'Network.requestWillBeSent')
202+
.then(([event]) => verifyRequestWillBeSent(event, { url }));
203+
session.on('Network.responseReceived', common.mustNotCall());
204+
session.on('Network.loadingFinished', common.mustNotCall());
205+
206+
const loadingFailedFuture = once(session, 'Network.loadingFailed')
207+
.then(([event]) => verifyLoadingFailed(event));
208+
209+
https.get({
210+
host: addresses.INVALID_HOST,
211+
headers: requestHeaders,
212+
}, common.mustNotCall()).on('error', common.mustCall());
213+
214+
await requestWillBeSentFuture;
215+
await loadingFailedFuture;
216+
}
217+
218+
const testNetworkInspection = async () => {
219+
await testHttpGet();
220+
session.removeAllListeners();
221+
await testHttpsGet();
222+
session.removeAllListeners();
223+
await testHttpError();
224+
session.removeAllListeners();
225+
await testHttpsError();
226+
session.removeAllListeners();
227+
};
228+
229+
httpServer.listen(0, () => {
230+
httpsServer.listen(0, async () => {
231+
try {
232+
await session.post('Network.enable');
233+
await testNetworkInspection();
234+
await session.post('Network.disable');
235+
} catch (e) {
236+
assert.fail(e);
237+
} finally {
238+
terminate();
239+
}
240+
});
241+
});

0 commit comments

Comments
 (0)
Please sign in to comment.