microsoft / microsoft/playwright
[Bug]: headersArray() splits single header values on commas on Firefox and WebKit, corrupting every HTTP-date header
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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