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..9b3252aa61a5c 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']); @@ -452,6 +460,61 @@ 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'); + console.log('HAR path:', harPath); + 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'); + 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'); +}); + +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'); + console.log('HAR path:', harPath); + 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();