From e7df68c809f15edd1dd87dc281299b2b1f38fa3e Mon Sep 17 00:00:00 2001 From: Weiller Carvalho Date: Fri, 21 Nov 2025 19:10:21 -0300 Subject: [PATCH] fix(headers): keep set-cookie expires intact on webkit --- .../src/utils/isomorphic/headers.ts | 43 ++++++++++++++++++- tests/library/unit/headers.spec.ts | 35 +++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 tests/library/unit/headers.spec.ts diff --git a/packages/playwright-core/src/utils/isomorphic/headers.ts b/packages/playwright-core/src/utils/isomorphic/headers.ts index e228e5312f230..402523656bd2e 100644 --- a/packages/playwright-core/src/utils/isomorphic/headers.ts +++ b/packages/playwright-core/src/utils/isomorphic/headers.ts @@ -26,8 +26,10 @@ export function headersObjectToArray(headers: HeadersObject, separator?: string, if (values === undefined) continue; if (separator) { - const sep = name.toLowerCase() === 'set-cookie' ? setCookieSeparator : separator; - for (const value of values.split(sep!)) + const lowerCaseName = name.toLowerCase(); + const sep = lowerCaseName === 'set-cookie' ? setCookieSeparator : separator; + const splitValues = lowerCaseName === 'set-cookie' ? splitSetCookieHeader(values, sep!) : values.split(sep!); + for (const value of splitValues) result.push({ name, value: value.trim() }); } else { result.push({ name, value: values }); @@ -42,3 +44,40 @@ export function headersArrayToObject(headers: HeadersArray, lowerCase: boolean): result[lowerCase ? name.toLowerCase() : name] = value; return result; } + +function splitSetCookieHeader(value: string, separator: string): string[] { + if (!separator || !value.includes(separator)) + return [value]; + if (separator !== ',') + return value.split(separator); + const result: string[] = []; + let lastIndex = 0; + let inExpires = false; + const lowerValue = value.toLowerCase(); + for (let i = 0; i < value.length; ++i) { + if (!inExpires && lowerValue.startsWith('expires=', i)) + inExpires = true; + if (value[i] === ';') + inExpires = false; + if (value[i] === ',' && !inExpires && looksLikeCookieStart(value, i + 1)) { + result.push(value.substring(lastIndex, i).trim()); + lastIndex = i + 1; + } + } + result.push(value.substring(lastIndex).trim()); + return result.filter(v => v); +} + +function looksLikeCookieStart(header: string, start: number): boolean { + const rest = header.substring(start).trimStart(); + const eqIndex = rest.indexOf('='); + if (eqIndex <= 0) + return false; + const semicolonIndex = rest.indexOf(';'); + if (semicolonIndex !== -1 && semicolonIndex < eqIndex) + return false; + const name = rest.substring(0, eqIndex).trim(); + if (!name) + return false; + return !/[\s;,]/.test(name); +} diff --git a/tests/library/unit/headers.spec.ts b/tests/library/unit/headers.spec.ts new file mode 100644 index 0000000000000..78d76f6d7383c --- /dev/null +++ b/tests/library/unit/headers.spec.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from '@playwright/test'; +import { headersObjectToArray } from '../../../packages/playwright-core/src/utils/isomorphic/headers'; + +test.describe('headersObjectToArray set-cookie splitting', () => { + test('keeps expires date comma intact for single cookie', () => { + const value = 'id=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/'; + const result = headersObjectToArray({ 'Set-Cookie': value }, ',', ','); + expect(result).toEqual([{ name: 'Set-Cookie', value }]); + }); + + test('splits multiple cookies combined by WebKit comma separator', () => { + const value = 'id=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/, session=abc; Secure'; + const result = headersObjectToArray({ 'Set-Cookie': value }, ',', ','); + expect(result).toEqual([ + { name: 'Set-Cookie', value: 'id=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/' }, + { name: 'Set-Cookie', value: 'session=abc; Secure' }, + ]); + }); +});