From 677d1fafb7500a7170dd5bf127a3b65f8de86961 Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Mon, 14 Sep 2026 12:54:33 -0600 Subject: [PATCH] fix(network): preserve dates in response headers response header parsing corrupts HTTP dates and macOS WebKit `Set-Cookie` values preserve date headers and only split combined cookies before another cookie pair --- packages/isomorphic/headers.ts | 21 +++++++- .../src/server/firefox/ffNetworkManager.ts | 3 +- tests/page/page-network-response.spec.ts | 54 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/isomorphic/headers.ts b/packages/isomorphic/headers.ts index e228e5312f230..8876a6be783de 100644 --- a/packages/isomorphic/headers.ts +++ b/packages/isomorphic/headers.ts @@ -27,7 +27,7 @@ export function headersObjectToArray(headers: HeadersObject, separator?: string, continue; if (separator) { const sep = name.toLowerCase() === 'set-cookie' ? setCookieSeparator : separator; - for (const value of values.split(sep!)) + for (const value of splitHeaderValue(name, values, sep!)) result.push({ name, value: value.trim() }); } else { result.push({ name, value: values }); @@ -42,3 +42,22 @@ export function headersArrayToObject(headers: HeadersArray, lowerCase: boolean): result[lowerCase ? name.toLowerCase() : name] = value; return result; } + +export function splitHeaderValue(name: string, value: string, separator: string): string[] { + if (separator === ',') { + switch (name.toLowerCase()) { + case 'date': + case 'expires': + case 'last-modified': + case 'if-modified-since': + case 'if-unmodified-since': + case 'if-range': + case 'retry-after': + return [value]; + case 'set-cookie': + // Only split before a cookie pair, not the date following an Expires comma. + return value.split(/,(?=\s*[!#$%&'*+\-.^_`|~\da-z]+=)/i); + } + } + return value.split(separator); +} diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index e17afd5c45e69..39eefbd04c436 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { splitHeaderValue } from '@isomorphic/headers'; import { eventsHelper } from '@utils/eventsHelper'; import * as network from '../network'; @@ -281,7 +282,7 @@ function parseMultivalueHeaders(headers: HeadersArray) { const result: HeadersArray = []; for (const header of headers) { const separator = header.name.toLowerCase() === 'set-cookie' ? '\n' : ','; - const tokens = header.value.split(separator).map(s => s.trim()); + const tokens = splitHeaderValue(header.name, header.value, separator).map(s => s.trim()); for (const token of tokens) result.push({ name: header.name, value: token }); } diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index 9ec3e8eb6121d..bf7021fcca5c1 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -236,6 +236,35 @@ it('should report all headers', async ({ page, server, browserName, platform, is expect(actualHeaders).toEqual(expectedHeaders); }); +it('should preserve commas in HTTP date headers', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42687' }, +}, async ({ page, server }) => { + const date = 'Wed, 21 Oct 2015 07:28:00 GMT'; + const expectedHeaders = { + 'Date': date, + 'Expires': date, + 'Last-Modified': date, + 'If-Modified-Since': date, + 'If-Unmodified-Since': date, + 'If-Range': date, + 'Retry-After': date, + }; + server.setRoute('/headers', (req, res) => { + res.writeHead(200, expectedHeaders); + res.end('ok'); + }); + + const response = await page.goto(server.PREFIX + '/headers'); + const headers = await response.headersArray(); + const allHeaders = await response.allHeaders(); + for (const [name, value] of Object.entries(expectedHeaders)) { + expect(headers.filter(header => header.name.toLowerCase() === name.toLowerCase()).map(header => header.value)).toEqual([value]); + expect(await response.headerValues(name)).toEqual([value]); + expect(await response.headerValue(name)).toBe(value); + expect(allHeaders[name.toLowerCase()]).toBe(value); + } +}); + it('should report multiple set-cookie headers', async ({ page, server, isElectron, browserMajorVersion }) => { it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); @@ -260,6 +289,31 @@ it('should report multiple set-cookie headers', async ({ page, server, isElectro expect(await response.headerValues('set-cookie')).toEqual(['a=b', 'c=d']); }); +it('should preserve expires dates in set-cookie headers', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42687' }, +}, async ({ page, server, isElectron, browserMajorVersion }) => { + it.skip(isElectron && browserMajorVersion < 99, 'This needs Chromium >= 99'); + + const cookies = [ + 'first=value; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/', + 'second=value; Expires=Wed, 21 Oct 2015 07:28:00 GMT', + 'third=value', + ]; + for (const expectedCookies of [[cookies[0]], [cookies[1]], cookies]) { + server.setRoute('/headers', (req, res) => { + res.writeHead(200, { 'Set-Cookie': expectedCookies }); + res.end('ok'); + }); + + const response = await page.goto(server.PREFIX + '/headers'); + const headers = await response.headersArray(); + expect(headers.filter(({ name }) => name.toLowerCase() === 'set-cookie').map(({ value }) => value)).toEqual(expectedCookies); + expect(await response.headerValues('set-cookie')).toEqual(expectedCookies); + expect(await response.headerValue('set-cookie')).toBe(expectedCookies.join('\n')); + expect((await response.allHeaders())['set-cookie']).toBe(expectedCookies.join('\n')); + } +}); + it('should behave the same way for headers and allHeaders', async ({ page, server, browserName, platform }) => { it.skip(browserName === 'webkit' && platform === 'win32', 'libcurl does not support non-set-cookie multivalue headers'); server.setRoute('/headers', (req, res) => {