diff --git a/lib/internal/inspector/network_http.js b/lib/internal/inspector/network_http.js index 00b671cc4f8e7a..24c3598fd61763 100644 --- a/lib/internal/inspector/network_http.js +++ b/lib/internal/inspector/network_http.js @@ -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, }, }); diff --git a/lib/internal/inspector/network_undici.js b/lib/internal/inspector/network_undici.js index 636e2b21b45b4a..faa0bc35ec0462 100644 --- a/lib/internal/inspector/network_undici.js +++ b/lib/internal/inspector/network_undici.js @@ -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) 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, }, }); } diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index 497260ecc93c5f..a036714b1a4368 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -168,11 +168,23 @@ std::unique_ptr 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(); } diff --git a/src/inspector/node_protocol.pdl b/src/inspector/node_protocol.pdl index 2fe6634ad7e278..cb191ddaf19ffb 100644 --- a/src/inspector/node_protocol.pdl +++ b/src/inspector/node_protocol.pdl @@ -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 diff --git a/test/parallel/test-inspector-emit-protocol-event.js b/test/parallel/test-inspector-emit-protocol-event.js index e17e994ee520a3..2f3479fda78b8d 100644 --- a/test/parallel/test-inspector-emit-protocol-event.js +++ b/test/parallel/test-inspector-emit-protocol-event.js @@ -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' } } }, diff --git a/test/parallel/test-inspector-network-content-type.js b/test/parallel/test-inspector-network-content-type.js new file mode 100644 index 00000000000000..c8744e521fc5ec --- /dev/null +++ b/test/parallel/test-inspector-network-content-type.js @@ -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()); diff --git a/test/parallel/test-inspector-network-fetch.js b/test/parallel/test-inspector-network-fetch.js index 88585ab72bac75..cc16667c54a02d 100644 --- a/test/parallel/test-inspector-network-fetch.js +++ b/test/parallel/test-inspector-network-fetch.js @@ -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-')); diff --git a/test/parallel/test-inspector-network-http.js b/test/parallel/test-inspector-network-http.js index a02329891e1208..1dd4a65fc0dd72 100644 --- a/test/parallel/test-inspector-network-http.js +++ b/test/parallel/test-inspector-network-http.js @@ -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; }