inspector: add mimeType and charset support to Network.Response

Refs: https://github.com/nodejs/node/issues/53946
PR-URL: https://github.com/nodejs/node/pull/58192
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Shima Ryuhei 2025-05-28 19:15:37 +09:00 committed by GitHub
parent 3877800ffb
commit a4c7c9f6d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 232 additions and 2 deletions

View File

@ -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');
@ -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(),
@ -102,6 +115,8 @@ function onClientResponseFinish({ request, response }) {
status: response.statusCode,
statusText: response.statusMessage ?? '',
headers: convertHeaderObject(response.headers)[1],
mimeType,
charset,
},
});

View File

@ -1,6 +1,7 @@
'use strict';
const {
ArrayPrototypeFindIndex,
DateNow,
} = primordials;
@ -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) {
@ -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],
@ -102,6 +119,8 @@ function onClientResponseHeaders({ request, response }) {
status: response.statusCode,
statusText: response.statusText,
headers: responseHeadersArrayToDictionary(response.headers),
mimeType,
charset,
},
});
}

View File

@ -169,11 +169,23 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject(
return {};
}
protocol::String mimeType;
if (!ObjectGetProtocolString(context, response, "mimeType").To(&mimeType)) {
mimeType = protocol::String("");
}
protocol::String charset = protocol::String();
if (!ObjectGetProtocolString(context, response, "charset").To(&charset)) {
charset = protocol::String("");
}
return protocol::Network::Response::create()
.setUrl(url)
.setStatus(status)
.setStatusText(statusText)
.setHeaders(std::move(headers))
.setMimeType(mimeType)
.setCharset(charset)
.build();
}

View File

@ -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

View File

@ -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: {
@ -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'
}
}
},

View File

@ -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, '');
})
);
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());

View File

@ -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) => {
@ -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-'));
@ -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-'));

View File

@ -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;
@ -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;
}