diff --git a/packages/playwright-core/src/server/cookieStore.ts b/packages/playwright-core/src/server/cookieStore.ts index 5d54b6032b789..2d13ecb36ebea 100644 --- a/packages/playwright-core/src/server/cookieStore.ts +++ b/packages/playwright-core/src/server/cookieStore.ts @@ -153,6 +153,7 @@ export function parseRawCookie(header: string): RawCookie | null { name, value, }; + let maxAgeExpires: number | undefined; for (let i = 1; i < pairs.length; i++) { const [name, value] = pairs[i]; switch (name.toLowerCase()) { @@ -167,15 +168,16 @@ export function parseRawCookie(header: string): RawCookie | null { } break; case 'max-age': - const maxAgeSec = parseInt(value, 10); - if (isFinite(maxAgeSec)) { - // From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2 + // From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2 + // If the attribute-value is not an optional "-" followed by digits, ignore the cookie-av. + if (/^-?\d+$/.test(value)) { + const maxAgeSec = parseInt(value, 10); // If delta-seconds is less than or equal to zero (0), let expiry-time // be the earliest representable date and time. if (maxAgeSec <= 0) - cookie.expires = 0; + maxAgeExpires = 0; else - cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds); + maxAgeExpires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds); } break; case 'domain': @@ -207,6 +209,10 @@ export function parseRawCookie(header: string): RawCookie | null { break; } } + // https://datatracker.ietf.org/doc/html/rfc6265#section-5.3 + // Max-Age takes precedence over Expires, regardless of the order of attributes. + if (maxAgeExpires !== undefined) + cookie.expires = maxAgeExpires; return cookie; } diff --git a/tests/library/global-fetch-cookie.spec.ts b/tests/library/global-fetch-cookie.spec.ts index 8503c13784c08..5b54d2485b42d 100644 --- a/tests/library/global-fetch-cookie.spec.ts +++ b/tests/library/global-fetch-cookie.spec.ts @@ -313,6 +313,45 @@ it('should remove cookie with negative max-age', async ({ request, server }) => expect(serverRequest.headers.cookie).toBe('c=v'); }); +it('should prefer max-age over expires regardless of the order', async ({ request, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/43077' }); + + const past = 'Thu, 01 Jan 1970 00:00:00 GMT'; + const future = new Date(Date.now() + 3600_000).toUTCString(); + server.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Set-Cookie', [ + `a=v; max-age=3600; expires=${past}`, + `b=v; expires=${past}; max-age=3600`, + `c=v; max-age=-1; expires=${future}`, + `d=v; expires=${future}; max-age=-1`, + ]); + res.end(); + }); + await request.get(`${server.PREFIX}/setcookie.html`); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.cookie).toBe('a=v; b=v'); +}); + +it('should ignore max-age that is not a number', async ({ request, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/43077' }); + + server.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Set-Cookie', ['a=v; max-age=-1abc', 'b=v; max-age=3600abc', 'c=v; max-age=']); + res.end(); + }); + await request.get(`${server.PREFIX}/setcookie.html`); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + request.get(server.EMPTY_PAGE) + ]); + expect(serverRequest.headers.cookie).toBe('a=v; b=v; c=v'); + const { cookies } = await request.storageState(); + expect(cookies.map(c => c.expires)).toEqual([-1, -1, -1]); +}); + it('should remove cookie with expires far in the past', async ({ request, server }) => { server.setRoute('/setcookie.html', (req, res) => { res.setHeader('Set-Cookie', ['a=v; max-age=1000000']);