Skip to content

inspector: add mimeType and charset support to Network.Response #58192

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions lib/internal/inspector/network_http.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
} = require('internal/inspector/network');
const dc = require('diagnostics_channel');
const { Network } = require('inspector');
const { MIMEType } = require('internal/mime');

const kRequestUrl = Symbol('kRequestUrl');

Expand Down Expand Up @@ -93,6 +94,18 @@ function onClientResponseFinish({ request, response }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}

let mimeType;
let charset;
try {
const mimeTypeObj = new MIMEType(response.headers['content-type']);
mimeType = mimeTypeObj.essence || '';
charset = mimeTypeObj.params.get('charset') || '';
} catch {
mimeType = '';
charset = '';
}

Network.responseReceived({
requestId: request[kInspectorRequestId],
timestamp: getMonotonicTime(),
Expand All @@ -102,6 +115,8 @@ function onClientResponseFinish({ request, response }) {
status: response.statusCode,
statusText: response.statusMessage ?? '',
headers: convertHeaderObject(response.headers)[1],
mimeType,
charset,
},
});

Expand Down
19 changes: 19 additions & 0 deletions lib/internal/inspector/network_undici.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayPrototypeFindIndex,
DateNow,
} = primordials;

Expand All @@ -12,6 +13,7 @@ const {
} = require('internal/inspector/network');
const dc = require('diagnostics_channel');
const { Network } = require('inspector');
const { MIMEType } = require('internal/mime');

// Convert an undici request headers array to a plain object (Map<string, string>)
function requestHeadersArrayToDictionary(headers) {
Expand Down Expand Up @@ -91,6 +93,21 @@ function onClientResponseHeaders({ request, response }) {
if (typeof request[kInspectorRequestId] !== 'string') {
return;
}

let mimeType;
let charset;
try {
const contentTypeKeyIndex =
ArrayPrototypeFindIndex(response.headers, (header) => header.toString().toLowerCase() === 'content-type');
const contentType = contentTypeKeyIndex !== -1 ? response.headers[contentTypeKeyIndex + 1].toString() : '';
const mimeTypeObj = new MIMEType(contentType);
mimeType = mimeTypeObj.essence || '';
charset = mimeTypeObj.params.get('charset') || '';
} catch {
mimeType = '';
charset = '';
}

const url = `${request.origin}${request.path}`;
Network.responseReceived({
requestId: request[kInspectorRequestId],
Expand All @@ -102,6 +119,8 @@ function onClientResponseHeaders({ request, response }) {
status: response.statusCode,
statusText: response.statusText,
headers: responseHeadersArrayToDictionary(response.headers),
mimeType,
charset,
},
});
}
Expand Down
12 changes: 12 additions & 0 deletions src/inspector/network_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,23 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject(
return {};
}

protocol::String mimeType;
if (!ObjectGetProtocolString(context, response, "mimeType").To(&mimeType)) {
return {};
}

protocol::String charset = protocol::String();
if (!ObjectGetProtocolString(context, response, "charset").To(&charset)) {
return {};
}

return protocol::Network::Response::create()
.setUrl(url)
.setStatus(status)
.setStatusText(statusText)
.setHeaders(std::move(headers))
.setMimeType(mimeType)
.setCharset(charset)
.build();
}

Expand Down
2 changes: 2 additions & 0 deletions src/inspector/node_protocol.pdl
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ experimental domain Network
integer status
string statusText
Headers headers
string mimeType
string charset

# Request / response headers as keys / values of JSON object.
type Headers extends object
Expand Down
8 changes: 6 additions & 2 deletions test/parallel/test-inspector-emit-protocol-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ const EXPECTED_EVENTS = {
url: 'https://nodejs.org/en',
status: 200,
statusText: '',
headers: { host: 'nodejs.org' }
headers: { host: 'nodejs.org' },
mimeType: 'text/html',
charset: 'utf-8'
}
},
expected: {
Expand All @@ -53,7 +55,9 @@ const EXPECTED_EVENTS = {
url: 'https://nodejs.org/en',
status: 200,
statusText: '',
headers: { host: 'nodejs.org' }
headers: { host: 'nodejs.org' },
mimeType: 'text/html',
charset: 'utf-8'
}
}
},
Expand Down
170 changes: 170 additions & 0 deletions test/parallel/test-inspector-network-content-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Flags: --inspect=0 --experimental-network-inspection
'use strict';
const common = require('../common');

common.skipIfInspectorDisabled();

const assert = require('node:assert');
const http = require('node:http');
const inspector = require('node:inspector/promises');

const testNetworkInspection = async (session, port, assert) => {
let assertPromise = assert(session);
fetch(`http://127.0.0.1:${port}/hello-world`).then(common.mustCall());
await assertPromise;
session.removeAllListeners();
assertPromise = assert(session);
new Promise((resolve, reject) => {
const req = http.get(
{
host: '127.0.0.1',
port,
path: '/hello-world',
},
common.mustCall((res) => {
res.on('data', () => {});
res.on('end', () => {});
resolve(res);
})
);
req.on('error', reject);
});
await assertPromise;
session.removeAllListeners();
};

const test = (handleRequest, testSessionFunc) => new Promise((resolve) => {
const session = new inspector.Session();
session.connect();
const httpServer = http.createServer(handleRequest);
httpServer.listen(0, async () => {
try {
await session.post('Network.enable');
await testNetworkInspection(
session,
httpServer.address().port,
testSessionFunc
);
await session.post('Network.disable');
} catch (err) {
assert.fail(err);
} finally {
await session.disconnect();
await httpServer.close();
await inspector.close();
resolve();
}
});
});

(async () => {
await test(
(req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.writeHead(200);
res.end('hello world\n');
},
common.mustCall(
(session) =>
new Promise((resolve) => {
session.on(
'Network.responseReceived',
common.mustCall(({ params }) => {
assert.strictEqual(params.response.mimeType, 'text/plain');
assert.strictEqual(params.response.charset, 'utf-8');
})
);
session.on(
'Network.loadingFinished',
common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(typeof params.timestamp, 'number');
resolve();
})
);
}),
2
)
);

await test(
(req, res) => {
res.writeHead(200, {});
res.end('hello world\n');
},
common.mustCall((session) =>
new Promise((resolve) => {
session.on(
'Network.responseReceived',
common.mustCall(({ params }) => {
assert.strictEqual(params.response.mimeType, '');
assert.strictEqual(params.response.charset, '');
})
);
session.on(
'Network.loadingFinished',
common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(typeof params.timestamp, 'number');
resolve();
})
);
}), 2
)
);

await test(
(req, res) => {
res.setHeader('Content-Type', 'invalid content-type');
res.writeHead(200);
res.end('hello world\n');
},
common.mustCall((session) =>
new Promise((resolve) => {
session.on(
'Network.responseReceived',
common.mustCall(({ params }) => {
assert.strictEqual(params.response.mimeType, '');
assert.strictEqual(params.response.charset, '');
})
Comment on lines +126 to +129
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the Content-Type header is not set, Chromium also assigns an empty string.

);
session.on(
'Network.loadingFinished',
common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(typeof params.timestamp, 'number');
resolve();
})
);
}), 2
)
);

await test(
(req, res) => {
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
res.end('hello world\n');
},
common.mustCall((session) =>
new Promise((resolve) => {
session.on(
'Network.responseReceived',
common.mustCall(({ params }) => {
assert.strictEqual(params.response.mimeType, 'text/plain');
assert.strictEqual(params.response.charset, '');
})
);
session.on(
'Network.loadingFinished',
common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
assert.strictEqual(typeof params.timestamp, 'number');
resolve();
})
);
}), 2
)
);

})().then(common.mustCall());
5 changes: 5 additions & 0 deletions test/parallel/test-inspector-network-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const setResponseHeaders = (res) => {
res.setHeader('etag', 12345);
res.setHeader('Set-Cookie', ['key1=value1', 'key2=value2']);
res.setHeader('x-header2', ['value1', 'value2']);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
};

const handleRequest = (req, res) => {
Expand Down Expand Up @@ -101,6 +102,8 @@ const testHttpGet = () => new Promise((resolve, reject) => {
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');
assert.strictEqual(params.response.mimeType, 'text/plain');
assert.strictEqual(params.response.charset, 'utf-8');
}));
session.on('Network.loadingFinished', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
Expand Down Expand Up @@ -138,6 +141,8 @@ const testHttpsGet = () => new Promise((resolve, reject) => {
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');
assert.strictEqual(params.response.mimeType, 'text/plain');
assert.strictEqual(params.response.charset, 'utf-8');
}));
session.on('Network.loadingFinished', common.mustCall(({ params }) => {
assert.ok(params.requestId.startsWith('node-network-event-'));
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-inspector-network-http.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const setResponseHeaders = (res) => {
res.setHeader('etag', 12345);
res.setHeader('Set-Cookie', ['key1=value1', 'key2=value2']);
res.setHeader('x-header2', ['value1', 'value2']);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
};

const kTimeout = 1000;
Expand Down Expand Up @@ -106,6 +107,8 @@ function verifyResponseReceived({ method, params }, expect) {
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');
assert.strictEqual(params.response.mimeType, 'text/plain');
assert.strictEqual(params.response.charset, 'utf-8');

return params;
}
Expand Down
Loading