From c74843a9145fc0bda3f5f42b3a82249c36f60342 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 23 Jul 2024 18:28:34 -0700 Subject: [PATCH] chore(fetch): pass flush and finishFlush options to brotli (#31833) Fixes https://github.com/microsoft/playwright/issues/31814 --- packages/playwright-core/src/server/fetch.ts | 5 +- .../browsercontext-fetch-algorithms.spec.ts | 98 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 tests/library/browsercontext-fetch-algorithms.spec.ts diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 7a184e78a8..9064c7a84b 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -422,7 +422,10 @@ export abstract class APIRequestContext extends SdkObject { finishFlush: zlib.constants.Z_SYNC_FLUSH }); } else if (encoding === 'br') { - transform = zlib.createBrotliDecompress(); + transform = zlib.createBrotliDecompress({ + flush: zlib.constants.BROTLI_OPERATION_FLUSH, + finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH + }); } else if (encoding === 'deflate') { transform = zlib.createInflate(); } diff --git a/tests/library/browsercontext-fetch-algorithms.spec.ts b/tests/library/browsercontext-fetch-algorithms.spec.ts new file mode 100644 index 0000000000..dd46209f67 --- /dev/null +++ b/tests/library/browsercontext-fetch-algorithms.spec.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2014-present Matt Zabriskie + * Modifications copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { contextTest as it, expect } from '../config/browserTest'; +import util from 'util'; +import zlib from 'zlib'; + +const gzip = util.promisify(zlib.gzip); +const deflate = util.promisify(zlib.deflate); +const brotliCompress = util.promisify(zlib.brotliCompress); + +it.skip(({ mode }) => mode !== 'default'); + +it.describe('algorithms', () => { + const responseBody = 'str'; + + for (const [type, zipped] of Object.entries({ + gzip: gzip(responseBody), + deflate: deflate(responseBody), + br: brotliCompress(responseBody) + })) { + it.describe(`${type} decompression`, () => { + it(`should support decompression`, async ({ context, server }) => { + server.setRoute('/compressed', async (req, res) => { + res.setHeader('Content-Encoding', type); + res.end(await zipped); + }); + + const response = await context.request.get(server.PREFIX + '/compressed'); + expect(await response.text()).toEqual(responseBody); + }); + + it(`should not fail if response content-length header is missing (${type})`, async ({ context, server }) => { + server.setRoute('/compressed', async (req, res) => { + res.setHeader('Content-Encoding', type); + res.removeHeader('Content-Length'); + res.end(await zipped); + }); + + const response = await context.request.get(server.PREFIX + '/compressed'); + expect(await response.text()).toEqual(responseBody); + }); + + it('should not fail with chunked responses (without Content-Length header)', async ({ context, server }) => { + server.setRoute('/compressed', async (req, res) => { + res.setHeader('Content-Encoding', type); + res.setHeader('Transfer-Encoding', 'chunked'); + res.removeHeader('Content-Length'); + res.write(await zipped); + res.end(); + }); + + const response = await context.request.get(server.PREFIX + '/compressed'); + expect(await response.text()).toEqual(responseBody); + }); + + it('should not fail with an empty response without content-length header (Z_BUF_ERROR)', async ({ context, server }) => { + server.setRoute('/compressed', async (req, res) => { + res.setHeader('Content-Encoding', type); + res.removeHeader('Content-Length'); + res.end(); + }); + + const response = await context.request.get(server.PREFIX + '/compressed'); + expect(await response.text()).toEqual(''); + }); + + it('should not fail with an empty response with content-length header (Z_BUF_ERROR)', async ({ context, server }) => { + server.setRoute('/compressed', async (req, res) => { + res.setHeader('Content-Encoding', type); + res.end(); + }); + + await context.request.get(server.PREFIX + '/compressed'); + }); + }); + } +});