Skip to content

Commit 2a5c76c

Browse files
committed
fix: zipped responses
1 parent ce2ec3d commit 2a5c76c

File tree

5 files changed

+719
-35
lines changed

5 files changed

+719
-35
lines changed

lib/internal/inspector/network.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@ 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');
19+
const { Buffer } = require('buffer');
1320

1421
const kInspectorRequestId = Symbol('kInspectorRequestId');
22+
const kContentEncoding = Symbol('kContentEncoding');
1523

1624
// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-ResourceType
1725
const kResourceType = {
@@ -70,6 +78,69 @@ function sniffMimeType(contentType) {
7078
};
7179
}
7280

81+
/**
82+
* Gets the content encoding from the headers object.
83+
* @param {object} headers - The response headers.
84+
* @returns {string|undefined} - The content encoding (e.g., 'gzip', 'deflate', 'br', 'zstd').
85+
*/
86+
function getContentEncoding(headers = {}) {
87+
const contentEncoding = headers['content-encoding'];
88+
if (typeof contentEncoding === 'string') {
89+
return StringPrototypeToLowerCase(contentEncoding);
90+
}
91+
return undefined;
92+
}
93+
94+
/**
95+
* Creates a decompression stream based on the content encoding.
96+
* @param {string} encoding - The content encoding (e.g., 'gzip', 'deflate', 'br', 'zstd').
97+
* @returns {import('stream').Transform|null} - A decompression stream or null if encoding is not supported.
98+
*/
99+
function createDecompressor(encoding) {
100+
switch (encoding) {
101+
case 'gzip':
102+
case 'x-gzip':
103+
return createGunzip();
104+
case 'deflate':
105+
return createInflate();
106+
case 'br':
107+
return createBrotliDecompress();
108+
case 'zstd':
109+
return createZstdDecompress();
110+
default:
111+
return null;
112+
}
113+
}
114+
115+
/**
116+
* Decompresses a chunk of data based on the content encoding.
117+
* @param {Buffer} chunk - The compressed data chunk.
118+
* @param {string} encoding - The content encoding.
119+
* @param {function} callback - Callback with (error, decompressedChunk).
120+
*/
121+
function decompressChunk(chunk, encoding, callback) {
122+
const decompressor = createDecompressor(encoding);
123+
if (!decompressor) {
124+
// No decompression needed, return original chunk
125+
callback(null, chunk);
126+
return;
127+
}
128+
129+
const chunks = [];
130+
decompressor.on('data', (decompressedChunk) => {
131+
chunks.push(decompressedChunk);
132+
});
133+
decompressor.on('end', () => {
134+
callback(null, Buffer.concat(chunks));
135+
});
136+
decompressor.on('error', (err) => {
137+
// On decompression error, fall back to returning the original chunk
138+
callback(null, chunk);
139+
});
140+
141+
decompressor.end(chunk);
142+
}
143+
73144
function registerDiagnosticChannels(listenerPairs) {
74145
function enable() {
75146
ArrayPrototypeForEach(listenerPairs, ({ 0: channel, 1: listener }) => {
@@ -91,9 +162,13 @@ function registerDiagnosticChannels(listenerPairs) {
91162

92163
module.exports = {
93164
kInspectorRequestId,
165+
kContentEncoding,
94166
kResourceType,
95167
getMonotonicTime,
96168
getNextRequestId,
97169
registerDiagnosticChannels,
98170
sniffMimeType,
171+
getContentEncoding,
172+
createDecompressor,
173+
decompressChunk,
99174
};

lib/internal/inspector/network_http.js

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

1111
const {
1212
kInspectorRequestId,
13+
kContentEncoding,
1314
kResourceType,
1415
getMonotonicTime,
1516
getNextRequestId,
1617
registerDiagnosticChannels,
1718
sniffMimeType,
19+
getContentEncoding,
20+
createDecompressor,
1821
} = require('internal/inspector/network');
1922
const { Network } = require('inspector');
2023
const EventEmitter = require('events');
@@ -27,6 +30,7 @@ const convertHeaderObject = (headers = {}) => {
2730
let host;
2831
let charset;
2932
let mimeType;
33+
let contentEncoding;
3034
const dict = {};
3135
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
3236
const lowerCasedKey = key.toLowerCase();
@@ -38,6 +42,9 @@ const convertHeaderObject = (headers = {}) => {
3842
charset = result.charset;
3943
mimeType = result.mimeType;
4044
}
45+
if (lowerCasedKey === 'content-encoding') {
46+
contentEncoding = typeof value === 'string' ? value.toLowerCase() : undefined;
47+
}
4148
if (typeof value === 'string') {
4249
dict[key] = value;
4350
} else if (ArrayIsArray(value)) {
@@ -50,7 +57,7 @@ const convertHeaderObject = (headers = {}) => {
5057
dict[key] = String(value);
5158
}
5259
}
53-
return [dict, host, charset, mimeType];
60+
return [dict, host, charset, mimeType, contentEncoding];
5461
};
5562

5663
/**
@@ -105,7 +112,10 @@ function onClientResponseFinish({ request, response }) {
105112
return;
106113
}
107114

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

110120
Network.responseReceived({
111121
requestId: request[kInspectorRequestId],
@@ -121,24 +131,64 @@ function onClientResponseFinish({ request, response }) {
121131
},
122132
});
123133

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,
134+
// Create a decompressor if the response is compressed
135+
const decompressor = createDecompressor(contentEncoding);
136+
137+
if (decompressor) {
138+
// Pipe decompressed data to DevTools
139+
decompressor.on('data', (decompressedChunk) => {
140+
Network.dataReceived({
141+
requestId: request[kInspectorRequestId],
142+
timestamp: getMonotonicTime(),
143+
dataLength: decompressedChunk.byteLength,
144+
encodedDataLength: decompressedChunk.byteLength,
145+
data: decompressedChunk,
146+
});
132147
});
133-
});
134148

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

144194
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)