Skip to content

[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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions