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.
Version
1.64.0-next (
9cee42790), reproduced on chromium / firefox / webkit, macOS arm64Steps 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
Dateis affected on essentially every response.Expected
One entry holding the value that was actually sent, as chromium does:
Actual
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:
and for that same response
headersArray()reportsdateonce on chromium and twice on firefox and webkit, valued"Fri"and"11 Sep 2026 05:27:07 GMT".Second, macOS-only half: Set-Cookie
wkSetCookieSeparatoris','on darwin and a sentinel string elsewhere (wkInterceptableRequest.ts:45), so on macOS WebKit the same split also hitsSet-Cookie, which very commonly carries anExpiresdate: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-casedset-cookie.packages/playwright-core/src/server/firefox/ffNetworkManager.ts:280:WebKit reaches the same place through
headersObjectToArray(responsePayload.headers, ',', wkSetCookieSeparator)(wkInterceptableRequest.ts:88,wkPage.ts:407), andheadersObjectToArrayinpackages/isomorphic/headers.tsdoesvalues.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 wayset-cookieis 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
chunkedtransfer-encoding difference I noticed inrequest().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.