From 73ab5e5d22dea1e3d609710c70e2835e0884acde Mon Sep 17 00:00:00 2001 From: Victor Lin Date: Mon, 17 Nov 2025 15:13:30 -0800 Subject: [PATCH] fix(routeFromHAR): hack harRouter to merge set-cookie headers Route.fulfill currently does not support multiple headers with the same name (#37342). There are workarounds when using this API directly (merging headers, tweaking the header name casing, etc.), but this is problematic for routeFromHAR, which depends on this API internally. This patch adds special handling for set-cookie headers within harRouter to merge them into one header. There's some precedent for treating set-cookie specially at various places in the codebase (ex: https://github.com/microsoft/playwright/blob/f54478a23e0daa450fe524905eabc8aabf6efb07/packages/playwright-core/src/utils/isomorphic/headers.ts#L29, https://github.com/microsoft/playwright/blob/baeb065e9ea84502f347129a0b896a85d2a8dada/packages/playwright-core/src/server/chromium/crNetworkManager.ts#L675), so I think this is okay. --- packages/playwright-core/src/client/harRouter.ts | 9 ++++++++- tests/assets/har-fulfill.har | 8 ++++++++ tests/library/browsercontext-har.spec.ts | 8 ++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index a9ab966b2a574..7ac350f6f080a 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -68,9 +68,16 @@ export class HarRouter { // test when HAR was recorded but we'd abort it immediately. if (response.status === -1) return; + + // route.fulfill does not support multiple set-cookie headers. We need to merge them into one. + const setCookieHeaders = response.headers!.filter(h => h.name.toLowerCase() === 'set-cookie'); + const transformedHeaders = response.headers!.filter(h => h.name.toLowerCase() !== 'set-cookie'); + if (setCookieHeaders.length > 1) + transformedHeaders.push({ name: 'set-cookie', value: setCookieHeaders.map(h => h.value).join('\n') }); + await route.fulfill({ status: response.status, - headers: Object.fromEntries(response.headers!.map(h => [h.name, h.value])), + headers: Object.fromEntries(transformedHeaders.map(h => [h.name, h.value])), body: response.body! }); return; diff --git a/tests/assets/har-fulfill.har b/tests/assets/har-fulfill.har index 5b679098c878a..39261f5ed371a 100644 --- a/tests/assets/har-fulfill.har +++ b/tests/assets/har-fulfill.har @@ -62,6 +62,14 @@ { "name": "content-type", "value": "text/html" + }, + { + "name": "Set-Cookie", + "value": "playwright=works;" + }, + { + "name": "Set-Cookie", + "value": "with=multiple-set-cookie-headers;" } ], "content": { diff --git a/tests/library/browsercontext-har.spec.ts b/tests/library/browsercontext-har.spec.ts index 6e64045902508..5ff1f2c2e6140 100644 --- a/tests/library/browsercontext-har.spec.ts +++ b/tests/library/browsercontext-har.spec.ts @@ -358,6 +358,14 @@ it('should record overridden requests to har', async ({ contextFactory, server } expect(await page2.evaluate(fetchFunction, { path: '/echo', body: '12' })).toBe('12'); }); +it('should replay requests with multiple set-cookie headers properly', async ({ context, asset }) => { + const path = asset('har-fulfill.har'); + await context.routeFromHAR(path); + const page = await context.newPage(); + await page.goto('http://no.playwright/'); + expect(await page.context().cookies()).toEqual([expect.objectContaining({ name: 'playwright', value: 'works' }), expect.objectContaining({ name: 'with', value: 'multiple-set-cookie-headers' })]); +}); + it('should disambiguate by header', async ({ contextFactory, server }, testInfo) => { server.setRoute('/echo', async (req, res) => { res.end(req.headers['baz']);