Skip to content

Commit 186c7a9

Browse files
rubnogueiralegendecas
authored andcommitted
inspector: fix compressed responses
PR-URL: #61226 Fixes: #61222 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Kohei Ueno <kohei.ueno119@gmail.com>
1 parent 363758c commit 186c7a9

File tree

5 files changed

+703
-35
lines changed

5 files changed

+703
-35
lines changed

lib/internal/inspector/network.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@ const {
1010
const dc = require('diagnostics_channel');
1111
const { now } = require('internal/perf/utils');
1212
const { MIMEType } = require('internal/mime');
13+
const {
14+
createGunzip,
15+
createInflate,
16+
createBrotliDecompress,
17+
createZstdDecompress,
18+
} = require('zlib');
1319

1420
const kInspectorRequestId = Symbol('kInspectorRequestId');
21+
const kContentEncoding = Symbol('kContentEncoding');
1522

1623
// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-ResourceType
1724
const kResourceType = {
@@ -70,6 +77,27 @@ function sniffMimeType(contentType) {
7077
};
7178
}
7279

80+
/**
81+
* Creates a decompression stream based on the content encoding.
82+
* @param {string} encoding - The content encoding (e.g., 'gzip', 'deflate', 'br', 'zstd').
83+
* @returns {import('stream').Transform|null} - A decompression stream or null if encoding is not supported.
84+
*/
85+
function createDecompressor(encoding) {
86+
switch (encoding) {
87+
case 'gzip':
88+
case 'x-gzip':
89+
return createGunzip();
90+
case 'deflate':
91+
return createInflate();
92+
case 'br':
93+
return createBrotliDecompress();
94+
case 'zstd':
95+
return createZstdDecompress();
96+
default:
97+
return null;
98+
}
99+
}
100+
73101
function registerDiagnosticChannels(listenerPairs) {
74102
function enable() {
75103
ArrayPrototypeForEach(listenerPairs, ({ 0: channel, 1: listener }) => {
@@ -91,9 +119,11 @@ function registerDiagnosticChannels(listenerPairs) {
91119

92120
module.exports = {
93121
kInspectorRequestId,
122+
kContentEncoding,
94123
kResourceType,
95124
getMonotonicTime,
96125
getNextRequestId,
97126
registerDiagnosticChannels,
98127
sniffMimeType,
128+
createDecompressor,
99129
};

lib/internal/inspector/network_http.js

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ const {
1010

1111
const {
1212
kInspectorRequestId,
13+
kContentEncoding,
1314
kResourceType,
1415
getMonotonicTime,
1516
getNextRequestId,
1617
registerDiagnosticChannels,
1718
sniffMimeType,
19+
createDecompressor,
1820
} = require('internal/inspector/network');
1921
const { Network } = require('inspector');
2022
const EventEmitter = require('events');
@@ -27,6 +29,7 @@ const convertHeaderObject = (headers = {}) => {
2729
let host;
2830
let charset;
2931
let mimeType;
32+
let contentEncoding;
3033
const dict = {};
3134
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
3235
const lowerCasedKey = key.toLowerCase();
@@ -38,6 +41,9 @@ const convertHeaderObject = (headers = {}) => {
3841
charset = result.charset;
3942
mimeType = result.mimeType;
4043
}
44+
if (lowerCasedKey === 'content-encoding') {
45+
contentEncoding = typeof value === 'string' ? value.toLowerCase() : undefined;
46+
}
4147
if (typeof value === 'string') {
4248
dict[key] = value;
4349
} else if (ArrayIsArray(value)) {
@@ -50,7 +56,7 @@ const convertHeaderObject = (headers = {}) => {
5056
dict[key] = String(value);
5157
}
5258
}
53-
return [dict, host, charset, mimeType];
59+
return [dict, host, charset, mimeType, contentEncoding];
5460
};
5561

5662
/**
@@ -105,7 +111,10 @@ function onClientResponseFinish({ request, response }) {
105111
return;
106112
}
107113

108-
const { 0: headers, 2: charset, 3: mimeType } = convertHeaderObject(response.headers);
114+
const { 0: headers, 2: charset, 3: mimeType, 4: contentEncoding } = convertHeaderObject(response.headers);
115+
116+
// Store content encoding on the request for later use
117+
request[kContentEncoding] = contentEncoding;
109118

110119
Network.responseReceived({
111120
requestId: request[kInspectorRequestId],
@@ -121,24 +130,64 @@ function onClientResponseFinish({ request, response }) {
121130
},
122131
});
123132

124-
// Unlike response.on('data', ...), this does not put the stream into flowing mode.
125-
EventEmitter.prototype.on.call(response, 'data', (chunk) => {
126-
Network.dataReceived({
127-
requestId: request[kInspectorRequestId],
128-
timestamp: getMonotonicTime(),
129-
dataLength: chunk.byteLength,
130-
encodedDataLength: chunk.byteLength,
131-
data: chunk,
133+
// Create a decompressor if the response is compressed
134+
const decompressor = createDecompressor(contentEncoding);
135+
136+
if (decompressor) {
137+
// Pipe decompressed data to DevTools
138+
decompressor.on('data', (decompressedChunk) => {
139+
Network.dataReceived({
140+
requestId: request[kInspectorRequestId],
141+
timestamp: getMonotonicTime(),
142+
dataLength: decompressedChunk.byteLength,
143+
encodedDataLength: decompressedChunk.byteLength,
144+
data: decompressedChunk,
145+
});
132146
});
133-
});
134147

135-
// Wait until the response body is consumed by user code.
136-
response.once('end', () => {
137-
Network.loadingFinished({
138-
requestId: request[kInspectorRequestId],
139-
timestamp: getMonotonicTime(),
148+
// Handle decompression errors gracefully - fall back to raw data
149+
decompressor.on('error', () => {
150+
// If decompression fails, the raw data has already been sent via the fallback
140151
});
141-
});
152+
153+
// Unlike response.on('data', ...), this does not put the stream into flowing mode.
154+
EventEmitter.prototype.on.call(response, 'data', (chunk) => {
155+
// Feed the chunk into the decompressor
156+
decompressor.write(chunk);
157+
});
158+
159+
// Wait until the response body is consumed by user code.
160+
response.once('end', () => {
161+
// End the decompressor stream
162+
decompressor.end();
163+
decompressor.once('end', () => {
164+
Network.loadingFinished({
165+
requestId: request[kInspectorRequestId],
166+
timestamp: getMonotonicTime(),
167+
});
168+
});
169+
});
170+
} else {
171+
// No decompression needed, send data directly
172+
// Unlike response.on('data', ...), this does not put the stream into flowing mode.
173+
EventEmitter.prototype.on.call(response, 'data', (chunk) => {
174+
Network.dataReceived({
175+
requestId: request[kInspectorRequestId],
176+
timestamp: getMonotonicTime(),
177+
dataLength: chunk.byteLength,
178+
encodedDataLength: chunk.byteLength,
179+
data: chunk,
180+
});
181+
});
182+
183+
// Wait until the response body is consumed by user code.
184+
response.once('end', () => {
185+
Network.loadingFinished({
186+
requestId: request[kInspectorRequestId],
187+
timestamp: getMonotonicTime(),
188+
});
189+
});
190+
}
142191
}
143192

144193
module.exports = registerDiagnosticChannels([

lib/internal/inspector/network_http2.js

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ const {
1010

1111
const {
1212
kInspectorRequestId,
13+
kContentEncoding,
1314
kResourceType,
1415
getMonotonicTime,
1516
getNextRequestId,
1617
registerDiagnosticChannels,
1718
sniffMimeType,
19+
createDecompressor,
1820
} = require('internal/inspector/network');
1921
const { Network } = require('inspector');
2022
const {
2123
HTTP2_HEADER_AUTHORITY,
24+
HTTP2_HEADER_CONTENT_ENCODING,
2225
HTTP2_HEADER_CONTENT_TYPE,
2326
HTTP2_HEADER_COOKIE,
2427
HTTP2_HEADER_METHOD,
@@ -42,6 +45,7 @@ function convertHeaderObject(headers = {}) {
4245
let statusCode;
4346
let charset;
4447
let mimeType;
48+
let contentEncoding;
4549
const dict = {};
4650

4751
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
@@ -61,6 +65,8 @@ function convertHeaderObject(headers = {}) {
6165
const result = sniffMimeType(value);
6266
charset = result.charset;
6367
mimeType = result.mimeType;
68+
} else if (lowerCasedKey === HTTP2_HEADER_CONTENT_ENCODING) {
69+
contentEncoding = typeof value === 'string' ? value.toLowerCase() : undefined;
6470
}
6571

6672
if (typeof value === 'string') {
@@ -78,7 +84,7 @@ function convertHeaderObject(headers = {}) {
7884

7985
const url = `${scheme}://${authority}${path}`;
8086

81-
return [dict, url, method, statusCode, charset, mimeType];
87+
return [dict, url, method, statusCode, charset, mimeType, contentEncoding];
8288
}
8389

8490
/**
@@ -194,7 +200,16 @@ function onClientStreamFinish({ stream, headers }) {
194200
return;
195201
}
196202

197-
const { 0: convertedHeaderObject, 3: statusCode, 4: charset, 5: mimeType } = convertHeaderObject(headers);
203+
const {
204+
0: convertedHeaderObject,
205+
3: statusCode,
206+
4: charset,
207+
5: mimeType,
208+
6: contentEncoding,
209+
} = convertHeaderObject(headers);
210+
211+
// Store content encoding on the stream for later use
212+
stream[kContentEncoding] = contentEncoding;
198213

199214
Network.responseReceived({
200215
requestId: stream[kInspectorRequestId],
@@ -210,23 +225,56 @@ function onClientStreamFinish({ stream, headers }) {
210225
},
211226
});
212227

213-
// Unlike stream.on('data', ...), this does not put the stream into flowing mode.
214-
EventEmitter.prototype.on.call(stream, 'data', (chunk) => {
215-
/**
216-
* When a chunk of the response body has been received, cache it until `getResponseBody` request
217-
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody or
218-
* stream it with `streamResourceContent` request.
219-
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-streamResourceContent
220-
*/
221-
222-
Network.dataReceived({
223-
requestId: stream[kInspectorRequestId],
224-
timestamp: getMonotonicTime(),
225-
dataLength: chunk.byteLength,
226-
encodedDataLength: chunk.byteLength,
227-
data: chunk,
228+
// Create a decompressor if the response is compressed
229+
const decompressor = createDecompressor(contentEncoding);
230+
231+
if (decompressor) {
232+
// Pipe decompressed data to DevTools
233+
decompressor.on('data', (decompressedChunk) => {
234+
Network.dataReceived({
235+
requestId: stream[kInspectorRequestId],
236+
timestamp: getMonotonicTime(),
237+
dataLength: decompressedChunk.byteLength,
238+
encodedDataLength: decompressedChunk.byteLength,
239+
data: decompressedChunk,
240+
});
228241
});
229-
});
242+
243+
// Handle decompression errors gracefully
244+
decompressor.on('error', () => {
245+
// If decompression fails, the raw data has already been sent via the fallback
246+
});
247+
248+
// Unlike stream.on('data', ...), this does not put the stream into flowing mode.
249+
EventEmitter.prototype.on.call(stream, 'data', (chunk) => {
250+
// Feed the chunk into the decompressor
251+
decompressor.write(chunk);
252+
});
253+
254+
// End the decompressor when the stream closes
255+
stream.once('end', () => {
256+
decompressor.end();
257+
});
258+
} else {
259+
// No decompression needed, send data directly
260+
// Unlike stream.on('data', ...), this does not put the stream into flowing mode.
261+
EventEmitter.prototype.on.call(stream, 'data', (chunk) => {
262+
/**
263+
* When a chunk of the response body has been received, cache it until `getResponseBody` request
264+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody or
265+
* stream it with `streamResourceContent` request.
266+
* https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-streamResourceContent
267+
*/
268+
269+
Network.dataReceived({
270+
requestId: stream[kInspectorRequestId],
271+
timestamp: getMonotonicTime(),
272+
dataLength: chunk.byteLength,
273+
encodedDataLength: chunk.byteLength,
274+
data: chunk,
275+
});
276+
});
277+
}
230278
}
231279

232280
/**

0 commit comments

Comments
 (0)