Skip to content

Commit 186c7a9

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

5 files changed

Lines changed: 703 additions & 35 deletions

File tree

‎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
constdc=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
constkInspectorRequestId=Symbol('kInspectorRequestId');
21+
constkContentEncoding=Symbol('kContentEncoding');
1522

1623
// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-ResourceType
1724
constkResourceType={
@@ -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+
functioncreateDecompressor(encoding){
86+
switch(encoding){
87+
case'gzip':
88+
case'x-gzip':
89+
returncreateGunzip();
90+
case'deflate':
91+
returncreateInflate();
92+
case'br':
93+
returncreateBrotliDecompress();
94+
case'zstd':
95+
returncreateZstdDecompress();
96+
default:
97+
returnnull;
98+
}
99+
}
100+
73101
functionregisterDiagnosticChannels(listenerPairs){
74102
functionenable(){
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
constEventEmitter=require('events');
@@ -27,6 +29,7 @@ const convertHeaderObject = (headers = {}) => {
2729
lethost;
2830
letcharset;
2931
letmimeType;
32+
letcontentEncoding;
3033
constdict={};
3134
for(const{0: key,1: value}ofObjectEntries(headers)){
3235
constlowerCasedKey=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=typeofvalue==='string' ? value.toLowerCase() : undefined;
46+
}
4147
if(typeofvalue==='string'){
4248
dict[key]=value;
4349
}elseif(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+
constdecompressor=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
letstatusCode;
4346
letcharset;
4447
letmimeType;
48+
letcontentEncoding;
4549
constdict={};
4650

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

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

7985
consturl=`${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+
constdecompressor=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)