Skip to content

[Regression]: APIRequestContext JSON-encodes string data for "application/json;charset=UTF-8" since 1.65 (not in release notes) #43937

Closed

Description

@valerian12

Version

1.65.0 (same in 1.66.0-alpha-2026-11-05). Different in 1.64.1.

Steps to reproduce

// repro.mjs — "type": "module", @playwright/test installed
import http from 'node:http';
import { request } from '@playwright/test';

const srv = http.createServer((req, res) => { let b = ''; req.on('data', c => b += c); req.on('end', () => res.end(b)); });
await new Promise(r => srv.listen(0, r));
const url = `http://127.0.0.1:${srv.address().port}/`;
const ctx = await request.newContext();

for (const ct of ['application/json', 'application/json;charset=UTF-8', 'application/json; charset=utf-8', 'Application/JSON', 'application/problem+json']) {
  const r = await ctx.post(url, { data: 'plain text', headers: { 'content-type': ct } });
  console.log(ct, '->', JSON.stringify(await r.text()));
}
await ctx.dispose(); srv.close();

Expected behavior

Either the 1.64 behavior, or the change to be listed in the release notes. In 1.64.1 only an exact application/json header made Playwright JSON-encode a string data that is not valid JSON:

application/json                -> "\"plain text\""
application/json;charset=UTF-8  -> "plain text"
application/json; charset=utf-8 -> "plain text"
Application/JSON                -> "plain text"
application/problem+json        -> "plain text"

Actual behavior

In 1.65.0 the JSON content type is matched with parameters and case-insensitively, so the string is now JSON-encoded for these headers too:

application/json                -> "\"plain text\""
application/json;charset=UTF-8  -> "\"plain text\""
application/json; charset=utf-8 -> "\"plain text\""
Application/JSON                -> "\"plain text\""
application/problem+json        -> "plain text"

A string that parses as JSON ('{"a":1}') and a Buffer are sent as is in both versions.

Additional context

The new matching looks more correct, but it silently changes the request body. It is not mentioned in the 1.65 release notes.

What broke for us:

  • Negative API tests that send a deliberately malformed JSON body as a string (data: '{"a": ' with content-type: application/json;charset=UTF-8). The server now receives a valid JSON string literal instead of a malformed document, so the test checks something else (400 "Syntax error" becomes a different response).
  • Requests signed with HMAC over the raw body string: the signature is computed over {"a": , but "{\"a\": " is sent, so the server answers 401.

Our workaround is data: Buffer.from(body).

Requests:

  1. Please add this to the 1.65 breaking changes.
  2. Please document in APIRequestContext data that a string with a JSON content type is sent as is only when it parses as JSON, and is JSON-encoded otherwise. The docs currently read as if strings are always sent as is. Buffer is the way to send exact bytes.

Environment

OS: Linux (mcr.microsoft.com/playwright:v1.65.0-noble)
Node.js: 26.13.0
@playwright/test: 1.65.0

Activity

  1. github-actions commented on Nov 5, 2026

    @github-actions
  2. microsoft-playwright-automation commented on Nov 5, 2026

    @microsoft-playwright-automation
    Contributor

    Hi, I'm the Playwright bot. I reproduced this and prepared a documentation fix.

    Confirmed in 1.65.0 and current main. The matching change was intentional in #43395, but the release notes and data documentation did not explain its effect on strings. The proposal adds the 1.65 breaking-change entry and documents when strings are JSON-encoded and how to send exact bytes with Buffer. It does not change request behavior.

    Reproduction and checks

    Using the repository server fixture:

    const request = await playwright.request.newContext();
    const [req] = await Promise.all([
      server.waitForRequest('/empty.html'),
      request.post(server.EMPTY_PAGE, {
        headers: { 'content-type': 'application/json;charset=UTF-8' },
        data: '{"a": ',
      }),
    ]);
    console.log((await req.postBody).toString());
    await request.dispose();

    In 1.64.1 the body is {"a": . In 1.65.0 and main it is "{\"a\": ". Passing Buffer.from('{"a": ', 'utf8') preserves the original body in every version tested.

    I checked application/json, application/json;charset=UTF-8, application/json; charset=utf-8, Application/JSON and application/problem+json, each with malformed JSON, a valid JSON string and a Buffer:

    Version Result
    1.64.1 Only exact application/json JSON-encodes the malformed string.
    1.65.0 All four application/json variants JSON-encode it.
    1.66.0-next, main 17ec699e4f4760bab5ad95f6ef9b954d5f1ce7eb Same as 1.65.0 on Chromium, Firefox and WebKit test projects.

    application/problem+json stays raw; valid JSON strings and Buffers are unchanged. These standalone API requests do not use a browser engine.

    The change is commit 39ce04859ac1759611967ae76b5c590bf995149d, first released in 1.65.0. Existing request tests and the temporary reproduction passed across all three browser projects: 616 passed, 2 skipped. The build and npm run flint passed.

    Draft PR: #43945

    Triaged on Linux by the Playwright bot - agent run

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