Skip to content

Commit

Permalink
inspector: report loadingFinished until the response data is consumed
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
legendecas committed Dec 26, 2024
1 parent c94a9db commit 8980bb1
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 299 deletions.
31 changes: 31 additions & 0 deletions lib/internal/inspector/network.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

const {
NumberMAX_SAFE_INTEGER,
Symbol,
} = primordials;

const { now } = require('internal/perf/utils');
const kInspectorRequestId = Symbol('kInspectorRequestId');

/**
* Return a monotonically increasing time in seconds since an arbitrary point in the past.
* @returns {number}
*/
function getMonotonicTime() {
return now() / 1000;
}

let requestId = 0;
function getNextRequestId() {
if (requestId === NumberMAX_SAFE_INTEGER) {
requestId = 0;
}
return `node-network-event-${++requestId}`;
};

module.exports = {
kInspectorRequestId,
getMonotonicTime,
getNextRequestId,
};
132 changes: 132 additions & 0 deletions lib/internal/inspector/network_http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use strict';

const {
ArrayIsArray,
DateNow,
ObjectEntries,
String,
Symbol,
} = primordials;

const {
kInspectorRequestId,
getMonotonicTime,
getNextRequestId,
} = require('internal/inspector/network');
const dc = require('diagnostics_channel');
const { Network } = require('inspector');

const kResourceType = 'Other';
const kRequestUrl = Symbol('kRequestUrl');

// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
const convertHeaderObject = (headers = {}) => {
// The 'host' header that contains the host and port of the URL.
let host;
const dict = {};
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
if (key.toLowerCase() === 'host') {
host = value;
}
if (typeof value === 'string') {
dict[key] = value;
} else if (ArrayIsArray(value)) {
if (key.toLowerCase() === 'cookie') dict[key] = value.join('; ');
// ChromeDevTools frontend treats 'set-cookie' as a special case
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
else if (key.toLowerCase() === 'set-cookie') dict[key] = value.join('\n');
else dict[key] = value.join(', ');
} else {
dict[key] = String(value);
}
}
return [host, dict];
};

/**
* When a client request starts, emit Network.requestWillBeSent event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-requestWillBeSent
* @param {{ request: import('http').ClientRequest }} event
*/
function onClientRequestStart({ request }) {
request[kInspectorRequestId] = getNextRequestId();

const { 0: host, 1: headers } = convertHeaderObject(request.getHeaders());
const url = `${request.protocol}//${host}${request.path}`;
request[kRequestUrl] = url;

Network.requestWillBeSent({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
wallTime: DateNow(),
request: {
url,
method: request.method,
headers,
},
});
}

/**
* When a client request errors, emit Network.loadingFailed event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-loadingFailed
* @param {{ request: import('http').ClientRequest, error: any }} event
*/
function onClientRequestError({ request, error }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}
Network.loadingFailed({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
type: kResourceType,
errorText: error.message,
});
}

/**
* When response headers are received, emit Network.responseReceived event.
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
* @param {{ request: import('http').ClientRequest, error: any }} event
*/
function onClientResponseFinish({ request, response }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}
Network.responseReceived({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
type: kResourceType,
response: {
url: request[kRequestUrl],
status: response.statusCode,
statusText: response.statusMessage ?? '',
headers: convertHeaderObject(response.headers)[1],
},
});

// Wait until the response body is consumed by user code.
response.once('end', () => {
Network.loadingFinished({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
});
});
}

function enable() {
dc.subscribe('http.client.request.start', onClientRequestStart);
dc.subscribe('http.client.request.error', onClientRequestError);
dc.subscribe('http.client.response.finish', onClientResponseFinish);
}

function disable() {
dc.unsubscribe('http.client.request.start', onClientRequestStart);
dc.unsubscribe('http.client.request.error', onClientRequestError);
dc.unsubscribe('http.client.response.finish', onClientResponseFinish);
}

module.exports = {
enable,
disable,
};
99 changes: 6 additions & 93 deletions lib/internal/inspector_network_tracking.js
Original file line number Diff line number Diff line change
@@ -1,102 +1,15 @@
'use strict';

const {
ArrayIsArray,
DateNow,
ObjectEntries,
String,
} = primordials;

let dc;
let Network;

let requestId = 0;
const getNextRequestId = () => `node-network-event-${++requestId}`;

// Convert a Headers object (Map<string, number | string | string[]>) to a plain object (Map<string, string>)
const headerObjectToDictionary = (headers = {}) => {
const dict = {};
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
if (typeof value === 'string') {
dict[key] = value;
} else if (ArrayIsArray(value)) {
if (key.toLowerCase() === 'cookie') dict[key] = value.join('; ');
// ChromeDevTools frontend treats 'set-cookie' as a special case
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
else if (key.toLowerCase() === 'set-cookie') dict[key] = value.join('\n');
else dict[key] = value.join(', ');
} else {
dict[key] = String(value);
}
}
return dict;
};

function onClientRequestStart({ request }) {
const url = `${request.protocol}//${request.host}${request.path}`;
const wallTime = DateNow();
const timestamp = wallTime / 1000;
request._inspectorRequestId = getNextRequestId();
Network.requestWillBeSent({
requestId: request._inspectorRequestId,
timestamp,
wallTime,
request: {
url,
method: request.method,
headers: headerObjectToDictionary(request.getHeaders()),
},
});
}

function onClientRequestError({ request, error }) {
if (typeof request._inspectorRequestId !== 'string') {
return;
}
const timestamp = DateNow() / 1000;
Network.loadingFailed({
requestId: request._inspectorRequestId,
timestamp,
type: 'Other',
errorText: error.message,
});
}

function onClientResponseFinish({ request, response }) {
if (typeof request._inspectorRequestId !== 'string') {
return;
}
const url = `${request.protocol}//${request.host}${request.path}`;
const timestamp = DateNow() / 1000;
Network.responseReceived({
requestId: request._inspectorRequestId,
timestamp,
type: 'Other',
response: {
url,
status: response.statusCode,
statusText: response.statusMessage ?? '',
headers: headerObjectToDictionary(response.headers),
},
});
Network.loadingFinished({
requestId: request._inspectorRequestId,
timestamp,
});
}

function enable() {
dc ??= require('diagnostics_channel');
Network ??= require('inspector').Network;
dc.subscribe('http.client.request.start', onClientRequestStart);
dc.subscribe('http.client.request.error', onClientRequestError);
dc.subscribe('http.client.response.finish', onClientResponseFinish);
require('internal/inspector/network_http').enable();
// TODO: add undici request/websocket tracking.
// https://github.com/nodejs/node/issues/53946
}

function disable() {
dc.unsubscribe('http.client.request.start', onClientRequestStart);
dc.unsubscribe('http.client.request.error', onClientRequestError);
dc.unsubscribe('http.client.response.finish', onClientResponseFinish);
require('internal/inspector/network_http').disable();
// TODO: add undici request/websocket tracking.
// https://github.com/nodejs/node/issues/53946
}

module.exports = {
Expand Down
Loading

0 comments on commit 8980bb1

Please sign in to comment.