Repository navigation
[Bug]: headersArray() splits single header values on commas on Firefox and WebKit, corrupting every HTTP-date header #42687
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.
Activity
ashishb-automation commentedon Sep 13, 2026
Hi, I’d like to investigate this issue if it’s available for community contribution. I’ll first reproduce it across Chromium, Firefox and WebKit, then trace where the header values are being split. My proposed change would include a focused fix and regression coverage for HTTP-date and Set-Cookie values. Please let me know if this approach is acceptable before I begin.
ayaangazali commentedon Sep 13, 2026
Not mine to assign, and it is still unassigned, so that is a maintainer call rather than mine. Your plan sounds right to me though.
Two things that might save you time, since I burned some on both:
The Set-Cookie half is macOS-only.
wkSetCookieSeparatoris','on darwin and a sentinel string everywhere else (wkInterceptableRequest.ts:45), so on Linux and Windows WebKitSet-Cookieis already protected and only the other headers split. Firefox never splitsSet-Cookieat all, it uses'\n'for that one. So if you test only on Linux you will see a narrower bug than the report describes.The harder part is that the split is not gratuitous. Both protocols hand Playwright a single joined string per header name, so genuinely repeated headers have already been flattened by the time
parseMultivalueHeadersandheadersObjectToArraysee them, and splitting is an attempt to recover them. That is why I stopped short of sending a patch: excluding the HTTP-date headers the wayset-cookieis already excluded fixes the reported case cleanly, but it is a targeted fix rather than a general one, and the general one probably means getting raw header pairs out of juggler and the WebKit protocol. Worth deciding which of those you are aiming at before you write the regression tests, because it changes what they should assert.I have both backends set up locally, so if a comparison run against chromium would help at any point just say.
github-actions commentedon Sep 14, 2026
Hi, I'm the Playwright bot and I took a first look at this.
Reproduced on tip-of-tree (
1.64.0-next, sha52a0e72) on macOS arm64, exactly as described.headersArray()splits single-valued header values on,in Firefox and WebKit, so any HTTP-date header comes back as two bogus entries and neither holds the real value. Chromium is correct. Your reading of the source is right too —parseMultivalueHeadersinffNetworkManager.tsandheadersObjectToArray(..., ',', wkSetCookieSeparator)inwkInterceptableRequest.tsare the two places, andset-cookieis the only name excluded.The macOS-only second half reproduces as well: on this machine
wkSetCookieSeparatoris',', so WebKit splitsSet-Cookietoo, andallHeaders()['set-cookie']comes back with the comma turned into a newline inside the value. That one is the most damaging of the set, sinceallHeaders()is the API people reach for and it silently returns a corrupted string rather than an obviously-wrong split.What I ran and what came back
Condensed to a single spec in
tests/page/, run on macOS withnpm run test -- --project=chromium-page --project=firefox-page --project=webkit-page:chromium-pagepasses,firefox-pageandwebkit-pagefail. Logged values:allHeaders()['last-modified']andheaderValue('last-modified')are correct on all three, since both re-join — consistent with what you reported. I did not test Linux or Windows, butwkSetCookieSeparatoris','only ondarwin, so theSet-Cookiehalf being macOS-only matches the code.So: real bug, not deliberate approximation, and worth fixing. Your framing of the tradeoff is the right one — the split exists to un-flatten genuinely repeated headers after the protocol has already joined them, so excluding the single-valued HTTP-date names (and, on macOS WebKit,
Set-Cookie) fixes the common case without touching the protocol, while the general fix means getting raw header pairs out of juggler and the WebKit protocol. Which of those to take is a maintainer call, so I'll leave that open rather than guess at it.This is a first pass from a bot, so a maintainer may see it differently — but the report was unusually precise and the source pointers all checked out.
Triaged on macOS by the Playwright bot - agent run
mohanram-dev commentedon Sep 15, 2026
Submitted PR #42723 addressing this issue by preserving standard single-valued and HTTP-date headers without comma-splitting, as well as safely preserving dates inside \Expires\ attributes for comma-separated Set-Cookie headers on macOS WebKit.
dcrousso commentedon Sep 18, 2026
part of this is being fixed upstream WebKit/WebKit#74308
ayaangazali commentedon Sep 19, 2026
Now that #42723 is closed, one piece of hard evidence for whoever picks this up next, so the next attempt does not land in the same place.
I had asserted on this issue that the protocols hand Playwright headers already joined, so the comma split is an attempt to recover repeated headers rather than something gratuitous. That was inference, so I went and instrumented the raw juggler payload. It holds up. Serving two
x-multiheaders, twoSet-Cookieheaders and oneLast-Modifiedin the same response,Network.responseReceivedarrives as:The payload is typed as an array, which made me look twice, but it is one entry per header name, not per wire occurrence. Gecko has already joined the two
x-multiheaders with", "before Playwright sees them, which is exactly whyset-cookiegets a\nseparator and everything else gets,.So from that string alone
x-multi: m1, m2(two headers, must split) andlast-modified: Wed, 21 Oct ...(one header, must not split) are genuinely indistinguishable. Any rule that works purely on the joined value is guessing, which I think is the same thing Devin Rousso (@dcrousso) means by a broad approach having unexpected failures.That also lines up with
visitOriginalResponseHeadersbeing the direction. It is not infirefox/protocol.d.tstoday, so it reads as new juggler surface that would hand over the unjoined originals and remove the guess entirely, rather than a smarter way to split. If that is right then the fix here rides a Firefox roll and there is not much to do in this repo in the meantime.Happy to re-run my three-engine reproducer against whatever lands, I still have it set up.
fix(firefox): preserve response header field boundaries (#42832)