Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inspector: provide detailed network information to fix devtools frontend errors #54156

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion lib/internal/inspector_network_tracking.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use strict';

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

let dc;
Expand All @@ -10,6 +13,25 @@ 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();
Expand All @@ -22,18 +44,27 @@ function onClientRequestStart({ request }) {
request: {
url,
method: request.method,
headers: headerObjectToDictionary(request.getHeaders()),
},
});
}

function onClientResponseFinish({ request }) {
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,
legendecas marked this conversation as resolved.
Show resolved Hide resolved
statusText: response.statusMessage ?? '',
headers: headerObjectToDictionary(response.headers),
},
});
Network.loadingFinished({
requestId: request._inspectorRequestId,
Expand Down
60 changes: 54 additions & 6 deletions src/inspector/network_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,28 @@ namespace node {
namespace inspector {
namespace protocol {

std::unique_ptr<Network::Request> Request(const String& url,
const String& method) {
return Network::Request::create().setUrl(url).setMethod(method).build();
std::unique_ptr<Network::Request> createRequest(
const String& url,
const String& method,
std::unique_ptr<Network::Headers> headers) {
return Network::Request::create()
.setUrl(url)
.setMethod(method)
.setHeaders(std::move(headers))
.build();
}

std::unique_ptr<Network::Response> createResponse(
const String& url,
int status,
const String& statusText,
std::unique_ptr<Network::Headers> headers) {
return Network::Response::create()
.setUrl(url)
.setStatus(status)
.setStatusText(statusText)
.setHeaders(std::move(headers))
.build();
}

NetworkAgent::NetworkAgent(NetworkInspector* inspector)
Expand Down Expand Up @@ -55,8 +74,17 @@ void NetworkAgent::requestWillBeSent(
String method;
request->getString("method", &method);

frontend_->requestWillBeSent(
request_id, Request(url, method), timestamp, wall_time);
ErrorSupport errors;
auto headers =
Network::Headers::fromValue(request->getObject("headers"), &errors);
if (errors.hasErrors()) {
headers = std::make_unique<Network::Headers>(DictionaryValue::create());
}

frontend_->requestWillBeSent(request_id,
createRequest(url, method, std::move(headers)),
timestamp,
wall_time);
}

void NetworkAgent::responseReceived(
Expand All @@ -65,8 +93,28 @@ void NetworkAgent::responseReceived(
params->getString("requestId", &request_id);
double timestamp;
params->getDouble("timestamp", &timestamp);
String type;
params->getString("type", &type);
auto response = params->getObject("response");
String url;
response->getString("url", &url);
int status;
response->getInteger("status", &status);
String statusText;
response->getString("statusText", &statusText);

ErrorSupport errors;
auto headers =
Network::Headers::fromValue(response->getObject("headers"), &errors);
if (errors.hasErrors()) {
headers = std::make_unique<Network::Headers>(DictionaryValue::create());
}

frontend_->responseReceived(request_id, timestamp);
frontend_->responseReceived(
request_id,
timestamp,
type,
createResponse(url, status, statusText, std::move(headers)));
}

void NetworkAgent::loadingFinished(
Expand Down
3 changes: 0 additions & 3 deletions src/inspector/network_agent.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ class NetworkInspector;

namespace protocol {

std::unique_ptr<Network::Request> Request(const String& url,
const String& method);

class NetworkAgent : public Network::Backend {
public:
explicit NetworkAgent(NetworkInspector* inspector);
Expand Down
38 changes: 38 additions & 0 deletions src/inspector/node_protocol.pdl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,28 @@ experimental domain NodeWorker
# Partial support for Network domain of ChromeDevTools Protocol.
# https://chromedevtools.github.io/devtools-protocol/tot/Network
experimental domain Network
# Resource type as it was perceived by the rendering engine.
type ResourceType extends string
enum
Document
Stylesheet
Image
Media
Font
Script
TextTrack
XHR
Fetch
Prefetch
EventSource
WebSocket
Manifest
SignedExchange
Ping
CSPViolationReport
Preflight
Other

# Unique request identifier.
type RequestId extends string

Expand All @@ -115,6 +137,18 @@ experimental domain Network
properties
string url
string method
Headers headers

# HTTP response data.
type Response extends object
properties
string url
integer status
string statusText
Headers headers

# Request / response headers as keys / values of JSON object.
type Headers extends object

# Disables network tracking, prevents network events from being sent to the client.
command disable
Expand All @@ -141,6 +175,10 @@ experimental domain Network
RequestId requestId
# Timestamp.
MonotonicTime timestamp
# Resource type.
ResourceType type
# Response data.
Response response

event loadingFinished
parameters
Expand Down
31 changes: 29 additions & 2 deletions test/parallel/test-inspector-emit-protocol-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ const EXPECTED_EVENTS = {
requestId: 'request-id-1',
request: {
url: 'https://nodejs.org/en',
method: 'GET'
method: 'GET',
},
timestamp: 1000,
wallTime: 1000,
},
expected: {
requestId: 'request-id-1',
request: {
url: 'https://nodejs.org/en',
method: 'GET',
headers: {} // Headers should be an empty object if not provided.
},
timestamp: 1000,
wallTime: 1000,
Expand All @@ -26,6 +36,23 @@ const EXPECTED_EVENTS = {
params: {
requestId: 'request-id-1',
timestamp: 1000,
type: 'Other',
response: {
url: 'https://nodejs.org/en',
status: 200,
headers: { host: 'nodejs.org' }
}
},
expected: {
requestId: 'request-id-1',
timestamp: 1000,
type: 'Other',
response: {
url: 'https://nodejs.org/en',
status: 200,
statusText: '', // Status text should be an empty string if not provided.
headers: { host: 'nodejs.org' }
}
}
},
{
Expand Down Expand Up @@ -68,7 +95,7 @@ const runAsyncTest = async () => {
for (const [domain, events] of Object.entries(EXPECTED_EVENTS)) {
for (const event of events) {
session.on(`${domain}.${event.name}`, common.mustCall(({ params }) => {
assert.deepStrictEqual(params, event.params);
assert.deepStrictEqual(params, event.expected ?? event.params);
}));
inspector[domain][event.name](event.params);
}
Expand Down
46 changes: 46 additions & 0 deletions test/parallel/test-inspector-network-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,25 @@ const inspector = require('node:inspector/promises');
const session = new inspector.Session();
session.connect();

const requestHeaders = {
'accept-language': 'en-US',
'Cookie': ['k1=v1', 'k2=v2'],
'age': 1000,
'x-header1': ['value1', 'value2']
};

const setResponseHeaders = (res) => {
res.setHeader('server', 'node');
res.setHeader('etag', 12345);
res.setHeader('Set-Cookie', ['key1=value1', 'key2=value2']);
res.setHeader('x-header2', ['value1', 'value2']);
};

const httpServer = http.createServer((req, res) => {
const path = req.url;
switch (path) {
case '/hello-world':
setResponseHeaders(res);
res.writeHead(200);
res.end('hello world\n');
break;
Expand All @@ -32,6 +47,7 @@ const httpsServer = https.createServer({
const path = req.url;
switch (path) {
case '/hello-world':
setResponseHeaders(res);
res.writeHead(200);
res.end('hello world\n');
break;
Expand All @@ -52,12 +68,26 @@ const testHttpGet = () => new Promise((resolve, reject) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(params.request.url, 'http://127.0.0.1/hello-world');
assert.strictEqual(params.request.method, 'GET');
assert.strictEqual(typeof params.request.headers, 'object');
assert.strictEqual(params.request.headers['accept-language'], 'en-US');
assert.strictEqual(params.request.headers.cookie, 'k1=v1; k2=v2');
assert.strictEqual(params.request.headers.age, '1000');
assert.strictEqual(params.request.headers['x-header1'], 'value1, value2');
assert.strictEqual(typeof params.timestamp, 'number');
assert.strictEqual(typeof params.wallTime, 'number');
}));
session.on('Network.responseReceived', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(typeof params.timestamp, 'number');
assert.strictEqual(params.type, 'Other');
assert.strictEqual(params.response.status, 200);
assert.strictEqual(params.response.statusText, 'OK');
assert.strictEqual(params.response.url, 'http://127.0.0.1/hello-world');
assert.strictEqual(typeof params.response.headers, 'object');
assert.strictEqual(params.response.headers.server, 'node');
assert.strictEqual(params.response.headers.etag, '12345');
assert.strictEqual(params.response.headers['set-cookie'], 'key1=value1\nkey2=value2');
assert.strictEqual(params.response.headers['x-header2'], 'value1, value2');
}));
session.on('Network.loadingFinished', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
Expand All @@ -69,6 +99,7 @@ const testHttpGet = () => new Promise((resolve, reject) => {
host: '127.0.0.1',
port: httpServer.address().port,
path: '/hello-world',
headers: requestHeaders
}, common.mustCall());
});

Expand All @@ -77,12 +108,26 @@ const testHttpsGet = () => new Promise((resolve, reject) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(params.request.url, 'https://127.0.0.1/hello-world');
assert.strictEqual(params.request.method, 'GET');
assert.strictEqual(typeof params.request.headers, 'object');
assert.strictEqual(params.request.headers['accept-language'], 'en-US');
assert.strictEqual(params.request.headers.cookie, 'k1=v1; k2=v2');
assert.strictEqual(params.request.headers.age, '1000');
assert.strictEqual(params.request.headers['x-header1'], 'value1, value2');
assert.strictEqual(typeof params.timestamp, 'number');
assert.strictEqual(typeof params.wallTime, 'number');
}));
session.on('Network.responseReceived', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(typeof params.timestamp, 'number');
assert.strictEqual(params.type, 'Other');
assert.strictEqual(params.response.status, 200);
assert.strictEqual(params.response.statusText, 'OK');
assert.strictEqual(params.response.url, 'https://127.0.0.1/hello-world');
assert.strictEqual(typeof params.response.headers, 'object');
assert.strictEqual(params.response.headers.server, 'node');
assert.strictEqual(params.response.headers.etag, '12345');
assert.strictEqual(params.response.headers['set-cookie'], 'key1=value1\nkey2=value2');
assert.strictEqual(params.response.headers['x-header2'], 'value1, value2');
}));
session.on('Network.loadingFinished', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
Expand All @@ -95,6 +140,7 @@ const testHttpsGet = () => new Promise((resolve, reject) => {
port: httpsServer.address().port,
path: '/hello-world',
rejectUnauthorized: false,
headers: requestHeaders,
}, common.mustCall());
});

Expand Down
Loading