microsoft / microsoft/playwright

[Bug]: headersArray() splits single header values on commas on Firefox and WebKit, corrupting every HTTP-date header

Open
#42,687 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage-mac
Dominant language
TypeScript
Stars
96.3k
Forks
6.5k
Avg merge
1d 6h
Merged PRs (30d)
180

Description

Version

1.64.0-next (9cee42790), reproduced on chromium / firefox / webkit, macOS arm64

Steps to reproduce

Any header whose value legitimately contains a comma is split into multiple bogus entries on Firefox and WebKit. HTTP-date values always contain one, so Date is affected on essentially every response.

import { test } from '@playwright/test';

test('header split', async ({ page, browserName, server }) => {
  server.setRoute('/h', (req, res) => {
    res.writeHead(200, { 'last-modified': 'Wed, 21 Oct 2026 07:28:00 GMT' });
    res.end('ok');
  });
  const resp = await page.goto(server.PREFIX + '/h');
  console.log(browserName, (await resp.headersArray()).filter(h => h.name.toLowerCase() === 'last-modified'));
});
Expected

One entry holding the value that was actually sent, as chromium does:

chromium [ { name: 'last-modified', value: 'Wed, 21 Oct 2026 07:28:00 GMT' } ]
Actual
firefox  [ { name: 'last-modified', value: 'Wed' },
           { name: 'last-modified', value: '21 Oct 2026 07:28:00 GMT' } ]
webkit   [ { name: 'Last-Modified', value: 'Wed' },
           { name: 'Last-Modified', value: '21 Oct 2026 07:28:00 GMT' } ]

Neither entry holds the real value, and a header that was sent once is reported twice.

I checked this against the literal bytes rather than trusting another API. Reading the same route over a raw socket, the server writes exactly one date header:

WIRE header lines -> ["content-type: text/plain","Date: Fri, 11 Sep 2026 05:27:07 GMT","Connection: close","Transfer-Encoding: chunked"]
WIRE date count   -> 1

and for that same response headersArray() reports date once on chromium and twice on firefox and webkit, valued "Fri" and "11 Sep 2026 05:27:07 GMT".

Second, macOS-only half: Set-Cookie

wkSetCookieSeparator is ',' on darwin and a sentinel string elsewhere (wkInterceptableRequest.ts:45), so on macOS WebKit the same split also hits Set-Cookie, which very commonly carries an Expires date:

res.writeHead(200, { 'set-cookie': ['sid=1; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/'] });
chromium headersArray -> [ { value: 'sid=1; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/' } ]
firefox  headersArray -> [ { value: 'sid=1; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/' } ]
webkit   headersArray -> [ { value: 'sid=1; Expires=Wed' },
                           { value: '21 Oct 2026 07:28:00 GMT; Path=/' } ]

webkit   allHeaders()['set-cookie'] -> "sid=1; Expires=Wed\n21 Oct 2026 07:28:00 GMT; Path=/"

That last line is the worst of it: allHeaders() is correct on chromium and firefox, but on macOS WebKit the comma has become a newline inside the value. headerValue() is correct everywhere, since it re-joins.

Root cause

Both backends split on , for every header except a special-cased set-cookie.

packages/playwright-core/src/server/firefox/ffNetworkManager.ts:280:

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());
    for (const token of tokens)
      result.push({ name: header.name, value: token });
  }
  return result;
}

WebKit reaches the same place through headersObjectToArray(responsePayload.headers, ',', wkSetCookieSeparator) (wkInterceptableRequest.ts:88, wkPage.ts:407), and headersObjectToArray in packages/isomorphic/headers.ts does values.split(sep).

The split is there to recover genuinely repeated headers after the protocol has already joined them into one string, so some of the information is lost before Playwright sees it and any un-joining is a guess. But comma is a list separator only for list-valued headers; the HTTP-date headers (Date, Expires, Last-Modified, If-Modified-Since, If-Unmodified-Since) are single-valued by definition and can never be split correctly. Excluding those the way set-cookie is already excluded would fix the common case without touching the protocol, though I suspect the fully correct fix is to have juggler and the WebKit protocol hand over raw header pairs instead of a joined string, and that is not something I can do from this repo.

I left the chunked transfer-encoding difference I noticed in request().sizes() out of this report to keep it to one thing.

I am a freshman in college doing my best to contribute something useful here, so if the split is deliberate and headersArray() is only ever meant to be approximate on these two browsers, please say so and I will drop it.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with parseMultivalueHeaders in packages/playwright-core/src/server/firefox/ffNetworkManager.ts and headersObjectToArray in packages/isomorphic/headers.ts, then trace the WebKit calls from wkInterceptableRequest.ts:88 and wkPage.ts:407. Run the supplied headersArray reproduction across Chromium, Firefox, and WebKit. Done means comma-containing HTTP-date values remain single entries while genuinely repeated headers continue to be represented correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.