Skip to content

[Bug]: Route.FulfillAsync computes content-length from the base64-encoded body length (~33% too large) #3336

Description

@LosDavidos

Version

1.61.0 (also verified on 1.58.0; the code is unchanged on main)

Steps to reproduce

When Route.FulfillAsync is called with BodyBytes (or Path) and no explicit
content-length header, the auto-computed content-length is the length of the
base64-encoded body, not the byte length — i.e. ~33% too large.

src/Playwright/Core/Route.cs, NormalizeFulfillParametersAsync:

else if (bodyContent != null)
{
    resultBody = Convert.ToBase64String(bodyContent);
    isBase64 = true;
    length = resultBody.Length;          // <-- length of the BASE64 STRING
}
...
if (length > 0 && !resultHeaders.ContainsKey("content-length"))
{
    resultHeaders["content-length"] = length.ToString(CultureInfo.InvariantCulture);
}

The Path branch has the same problem (length = resultBody.Length after
Convert.ToBase64String). The Node.js client computes it correctly
(packages/playwright-core/src/client/network.ts: length = buffer.length
before encoding), so this is specific to the .NET port. The server passes
the header through to the browser unchanged.

Minimal repro:

using System.Text;
using Microsoft.Playwright;

var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();

var bytes = Encoding.UTF8.GetBytes("window.x=1;"); // 11 bytes
await page.RouteAsync("**/a.js", r => r.FulfillAsync(new()
{
    Status = 200,
    BodyBytes = bytes,
    ContentType = "application/javascript",
}));

var responseTask = page.WaitForResponseAsync("**/a.js");
await page.SetContentAsync("<script src=\"https://example.test/a.js\"></script>");
var response = await responseTask;

Console.WriteLine($"body bytes: {bytes.Length}");
Console.WriteLine($"content-length header seen by the browser: {response.Headers["content-length"]}");

Output:

body bytes: 11
content-length header seen by the browser: 16

16 is exactly Convert.ToBase64String length of an 11-byte body. The Node.js
client produces content-length: 11 for the equivalent code.

Expected behavior

content-length matches the decoded body byte length, as in the Node.js client.

Actual behavior

content-length is the base64 string length (~4/3 of the real size), for every
fulfill that relies on the auto-computed header.

Additional context

Suggested fix: compute the length from the byte count before encoding
(content.Length in the Path branch, bodyContent.Length in the byte-array
branch). The string-body branch likely also wants
Encoding.UTF8.GetByteCount(body) instead of body.Length for non-ASCII bodies.

Real-world impact: in our production setup (Linux container, caching and
re-fulfilling large JS bundles of a site behind a proxy), fulfilled scripts in
the tens of MB stalled the page silently and indefinitely — no exception, no
console/page error — and the stall disappeared once we supplied the correct
content-length explicitly. We could not reproduce the stall in a minimal
synthetic scenario (a 30 MB fulfilled script with the inflated header loads
fine on Windows against a routed origin), so we are not claiming the stall
mechanism here — only the header mismatch, which is unambiguous and diverges
from the Node.js client.

Environment

- Microsoft.Playwright 1.61.0 / 1.58.0
- Windows 11 (repro above); production on .NET 10, Linux container (mcr.microsoft.com/playwright/dotnet:v1.61.0-noble)
- Chromium (bundled)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions