diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 7299449c041ad..28f9c8f41b10c 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -522,10 +522,19 @@ export abstract class APIRequestContext extends SdkObject { // Brotli and deflate decompressors throw if the input stream is empty. const emptyStreamTransform = new SafeEmptyStreamTransform(notifyBodyFinished); body = pipeline(response, emptyStreamTransform, transform, e => { - if (e) - reject(new Error(`failed to decompress '${encoding}' encoding: ${e.message}`)); + if (e) { + if (isNetworkConnectionError(e)) + reject(e); + else + reject(new Error(`failed to decompress '${encoding}' encoding: ${e.message}`)); + } + }); + body.on('error', e => { + if (isNetworkConnectionError(e)) + reject(e); + else + reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`)); }); - body.on('error', e => reject(new Error(`failed to decompress '${encoding}' encoding: ${e}`))); } else { body.on('error', reject); } @@ -804,6 +813,11 @@ function removeHeader(headers: { [name: string]: string }, name: string) { delete headers[existing[0]]; } +function isNetworkConnectionError(e: any): boolean { + const code = e?.code; + return code === 'ECONNRESET' || code === 'EPIPE' || code === 'ECONNABORTED'; +} + function setBasicAuthorizationHeader(headers: { [name: string]: string }, credentials: HTTPCredentials) { const { username, password } = credentials; const encoded = Buffer.from(`${username || ''}:${password || ''}`).toString('base64'); diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts index 871e435b6df70..2ab27fa3cbd25 100644 --- a/tests/library/browsercontext-fetch.spec.ts +++ b/tests/library/browsercontext-fetch.spec.ts @@ -1401,3 +1401,28 @@ it('should retry on ECONNRESET', { expect(await response.text()).toBe('Hello!'); expect(requestCount).toBe(4); }); + +it('should retry ECONNRESET on compressed response', async ({ context, server }) => { + let requestCount = 0; + server.setRoute('/test-gzip', (req, res) => { + if (requestCount++ < 2) { + req.socket.destroy(); + return; + } + res.writeHead(200, { + 'Content-Encoding': 'gzip', + 'Content-Type': 'text/plain', + }); + const gzipStream = zlib.createGzip(); + pipeline(gzipStream, res, err => { + if (err) + console.log(`Server error: ${err}`); + }); + gzipStream.write('compressed-retry-ok'); + gzipStream.end(); + }); + const response = await context.request.get(server.PREFIX + '/test-gzip', { maxRetries: 3 }); + expect(response.status()).toBe(200); + expect(await response.text()).toBe('compressed-retry-ok'); + expect(requestCount).toBe(3); +});