diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index a9ab966b2a574..f3f11f852e37a 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -68,9 +68,28 @@ 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 transformedHeaders = response.headers!.reduce((headersMap, { name, value }) => { + if (name.toLowerCase() !== 'set-cookie') { + // non-set-cookie header gets set as-is + headersMap[name] = value; + } else { + // first set-cookie header gets included as-is + if (!headersMap['set-cookie']) + headersMap['set-cookie'] = value; + else + // subsequent set-cookie headers get appended to existing header + headersMap['set-cookie'] += `\n${value}`; + + } + return headersMap; + }, {} as Record); + await route.fulfill({ status: response.status, - headers: Object.fromEntries(response.headers!.map(h => [h.name, h.value])), + headers: transformedHeaders, 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..f82bd30e87e43 100644 --- a/tests/library/browsercontext-har.spec.ts +++ b/tests/library/browsercontext-har.spec.ts @@ -452,6 +452,59 @@ it('should ignore boundary when matching multipart/form-data body', { await expect(page2.locator('div')).toHaveText('done'); }); +it('should record single set-cookie headers', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31495' } +}, async ({ contextFactory, server }, testInfo) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('set-cookie', ['first=foo']); + res.end(); + }); + + const harPath = testInfo.outputPath('har.zip'); + const context1 = await contextFactory(); + await context1.routeFromHAR(harPath, { update: true }); + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + const cookie1 = await page1.evaluate(() => document.cookie); + expect(cookie1).toBe('first=foo'); + await context1.close(); + + const context2 = await contextFactory(); + await context2.routeFromHAR(harPath, { notFound: 'abort' }); + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + const cookie2 = await page2.evaluate(() => document.cookie); + expect(cookie2).toBe('first=foo'); +}); + +it('should record multiple set-cookie headers', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31495' } +}, async ({ contextFactory, server }, testInfo) => { + server.setRoute('/empty.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.setHeader('set-cookie', ['first=foo', 'second=bar']); + res.end(); + }); + + const harPath = testInfo.outputPath('har.zip'); + const context1 = await contextFactory(); + await context1.routeFromHAR(harPath, { update: true }); + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + const cookie1 = await page1.evaluate(() => document.cookie); + expect(cookie1.split('; ').sort().join('; ')).toBe('first=foo; second=bar'); + await context1.close(); + + const context2 = await contextFactory(); + await context2.routeFromHAR(harPath, { notFound: 'abort' }); + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + const cookie2 = await page2.evaluate(() => document.cookie); + expect(cookie2.split('; ').sort().join('; ')).toBe('first=foo; second=bar'); +}); + + it('should update har.zip for page', async ({ contextFactory, server }, testInfo) => { const harPath = testInfo.outputPath('har.zip'); const context1 = await contextFactory();