Skip to content

fix(browser): decode response bodies as UTF-8 text in the run sandbox - #357

Merged
ankitranjan7 merged 2 commits into
mainfrom
fix/browser-run-response-text
Aug 19, 2026
Merged

fix(browser): decode response bodies as UTF-8 text in the run sandbox#357
ankitranjan7 merged 2 commits into
mainfrom
fix/browser-run-response-text

Conversation

@ankitranjan7

Copy link
Copy Markdown
Contributor

Closes #354

What was wrong

Response.text() in the vendored Playwright client did content.toString('utf8') on the body. Under QuickJS there is no Node Buffer, so the body is a plain Uint8Array and Uint8Array.prototype.toString falls through to Array.prototype.toString — comma-joining the byte values and discarding the 'utf8' argument. response.json() then JSON.parsed that string and always failed.

Request.postData() had the identical defect (?.toString('utf-8')), so it is fixed in the same commit.

The fix

quickjsEncoding.decodeText(...) from src/browser/run/playwright-client/quickjs-platform.ts, the same injected UTF-8 codec vendor/protocol/validatorPrimitives.ts already uses for base64.

Note: the issue suggested this._platform.decodeText(content). That would have been undefined at runtime — decodeText lives on the exported quickjsEncoding object, not on the Platform type. Importing quickjsEncoding is the pattern the vendored protocol files already follow.

src/browser/run/generated/playwright-client.js is regenerated with node scripts/build-playwright-sandbox-client.mjs, vendor-manifest.json's vendorSha256 is updated for the vendor edit, and the README's "Local patches" list documents it. playwright-client-build.test.ts needed no change; its --check passes.

Before / after

Same repro, same daemon, only the bundle swapped between runs.

Before:

{
  "type": "string",
  "first80": "123,34,105,100,34,58,49,44,34,116,105,116,108,101,34,58,34,69,115,115,101,110,99",
  "len": 5329,
  "jsonError": "unexpected data at the end"
}

After:

{
  "type": "string",
  "first80": "{\"id\":1,\"title\":\"Essence Mascara Lash Princess\",\"description\":\"The Essence Masca",
  "len": 1510,
  "id": 1,
  "title": "Essence Mascara Lash Princess"
}

Verified

  • End-to-end against https://dummyjson.com/products/1 through a daemon restarted from this branch; response.json() returns real fields.
  • New regression test in src/browser/run/runner.test.ts covering postData(), text(), and json() against the in-test example.test routes (no network). It fails with the old bundle (unexpected data at the end) and passes with the new one.
  • npx vitest run --project unit: 117 failures both before and after on the same checkout — identical set, all pre-existing/environment-related; no new failures, and the suite gains the one new passing test.
  • npx tsc --noEmit: clean.
  • node scripts/build-playwright-sandbox-client.mjs --check: clean.

Out of scope, noticed in passing

  • APIResponse.text() in vendor/client/fetch.ts has the same toString('utf8') bug, but it is unreachable: every APIResponse comes from _innerFetch, which evaluates options.params instanceof URLSearchParams unconditionally, and URLSearchParams is not defined in the sandbox — so page.request.get(...) throws ReferenceError before any response exists. Left untouched per browser run: response.text() returns comma-joined bytes, breaking response.json() #354's scope; the URLSearchParams gap is a separate bug worth its own issue.
  • Buffer.byteLength / Buffer.from remain in network.ts fulfill/override paths — same missing-Buffer family, different symptom, not touched here.
  • The /syntaxerror/i misclassification this bug triggers is browser run: syntax-error classifier mislabels runtime JSON.parse failures #355 and is deliberately untouched.

🤖 Generated with Claude Code

Response.text() called Buffer.prototype.toString('utf8') on the body, but
the QuickJS sandbox has no Buffer: the body is a plain Uint8Array, whose
inherited Array.prototype.toString comma-joins the elements and discards
the encoding argument. Every response.text() returned "123,34,105,..."
and response.json() failed with "unexpected data at the end".

Decode through quickjsEncoding.decodeText instead, the same injected UTF-8
codec the vendored protocol primitives already use. Request.postData() had
the identical defect and is fixed alongside it. APIResponse.text() in
fetch.ts shares the bug but is unreachable today, so it is left alone.

Closes #354

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🟠 Maintainer review suggested — low confidence

The automated review could not reach a fully supported conclusion.

Limitations

  • The automated review returned an invalid structured result.

This review is advisory and does not block merging.

`_innerFetch` evaluated `options.params instanceof URLSearchParams`
unconditionally, and QuickJS has no `URLSearchParams`, so every
`page.request.*` call threw `ReferenceError` before a response existed.
Guard the check behind `globalThis`, matching the `globalThis.FormData &&`
idiom upstream already uses a few lines below.

With the guard in place the rest of the path becomes reachable, so also
decode `APIResponse.text()` through the injected UTF-8 codec instead of
`Uint8Array.prototype.toString('utf8')` (which comma-joins bytes), and
treat request/multipart payloads as `Uint8Array` rather than `Buffer`,
which the sandbox also lacks.

Closes #359

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@ankitranjan7
ankitranjan7 merged commit 65d31cd into main Aug 19, 2026
37 checks passed
@ankitranjan7
ankitranjan7 deleted the fix/browser-run-response-text branch September 1, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

browser run: response.text() returns comma-joined bytes, breaking response.json()

1 participant