-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Open
Labels
browser-chromiumupstreamThis is a bug in something playwright depends on, like a browser.This is a bug in something playwright depends on, like a browser.
Description
Version
1.54.0
Steps to reproduce
git clone https://github.com/IamHuskar/playwright_sse_mojibake
npm install
npx playwright install
npm run repro
or
Start the server in one terminal:
npm run serverRun the client in another terminal:
npm run clientserver.ts
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
const port = 5000;
const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>SSE Test</title>
</head>
<body>
<h1>SSE Test Page</h1>
<button id="btn">Start SSE</button>
<script>
document.getElementById("btn").addEventListener("click", () => {
const evtSource = new EventSource("/sse");
evtSource.onmessage = event => console.log(event.data);
evtSource.onerror = () => evtSource.close();
});
</script>
</body>
</html>
`;
const messages = ["你好,这是第一条消息", "测试中文:"];
export function startServer() {
const server = createServer((req, res) => {
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
if (url.pathname === "/") {
res.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache"
});
res.end(html);
return;
}
if (url.pathname === "/sse") {
res.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive"
});
let index = 0;
const timer = setInterval(() => {
if (index >= messages.length) {
clearInterval(timer);
res.end();
return;
}
res.write(Buffer.from(`data: ${messages[index]}\n\n`, "utf8"));
index += 1;
}, 300);
req.on("close", () => {
clearInterval(timer);
res.end();
});
return;
}
res.writeHead(404, {
"Content-Type": "text/plain; charset=utf-8"
});
res.end("Not Found");
});
server.listen(port, () => {
console.log(`SSE server listening on http://localhost:${port}`);
});
return server;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
startServer();
}
client.ts
import { chromium } from "playwright";
import { fileURLToPath } from "node:url";
function printBuffer(label: string, body: Buffer) {
console.log(`\n[${label}]`);
console.log(` Raw bytes: ${body.toString("hex")}`);
try {
console.log(` Decoded: ${JSON.stringify(body.toString("utf8"))}`);
} catch (error) {
console.log(` Decoded: <decode failed: ${String(error)}>`);
}
}
export async function main() {
const headless = process.env.HEADLESS === "1";
const browser = await chromium.launch({ headless });
const page = await browser.newPage();
const cdp = await page.context().newCDPSession(page);
await cdp.send("Network.enable");
const sseRequestIds = new Set<string>();
cdp.on("Network.responseReceived", params => {
if (typeof params.response?.url === "string" && params.response.url.includes("/sse")) {
sseRequestIds.add(params.requestId);
}
});
cdp.on("Network.loadingFinished", async params => {
if (!sseRequestIds.has(params.requestId)) {
return;
}
try {
const result = await cdp.send("Network.getResponseBody", { requestId: params.requestId });
console.log("\n[CDP Network.getResponseBody()]");
console.log(` base64Encoded: ${result.base64Encoded}`);
console.log(` body type: ${typeof result.body}`);
console.log(` body: ${JSON.stringify(String(result.body).slice(0, 120))}`);
} catch (error) {
console.log(`\n[CDP Network.getResponseBody()] failed: ${String(error)}`);
}
});
await page.route("**/sse", async route => {
const response = await route.fetch();
const body = Buffer.from(await response.body());
printBuffer("route.fetch() - CORRECT", body);
await route.fulfill({ response });
});
page.on("response", async response => {
if (!response.url().includes("/sse")) {
return;
}
try {
const body = Buffer.from(await response.body());
printBuffer("response.body() - BUG", body);
} catch (error) {
console.log(`\n[response.body() - BUG] failed: ${String(error)}`);
}
});
await page.goto("http://localhost:5000");
await page.click("#btn");
await page.waitForTimeout(3000);
await browser.close();
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
}
Expected behavior
response.body() should return the same raw UTF-8 bytes as the server sent, matching route.fetch():
[route.fetch() - CORRECT]
Decoded: "data: 你好,这是第一条消息\n\ndata: 测试中文:\n\n"
[response.body() - CORRECT]
Decoded: "data: 你好,这是第一条消息\n\ndata: 测试中文:\n\n"
Actual behavior
response.body() returns mojibake / double-encoded bytes, while route.fetch() is correct:
[route.fetch() - CORRECT]
Raw bytes: 646174613a20e4bda0e5a5bdefbc8ce8bf99e698afe7acace4b880e69da1e6b688e681af0a0a646174613a20e6b58be8af95e4b8ade69687efbc9a0a0a
Decoded: "data: 你好,这是第一条消息\n\ndata: 测试中文:\n\n"
[response.body() - BUG]
Raw bytes: 646174613a20c3a4c2bdc2a0c3a5c2a5c2bdc3afc2bcc592c3a8c2bfe284a2c3a6cb9cc2afc3a7c2acc2acc3a4c2b8e282acc3a6c29dc2a1c3a6c2b6cb86c3a6c281c2af0a0a646174613a20c3a6c2b5e280b9c3a8c2afe280a2c3a4c2b8c2adc3a6e28093e280a1c3afc2bcc5a10a0a
Decoded: "data: ä½ å¥½ï¼Œè¿™æ˜¯ç¬¬ä¸€æ¡æ¶ˆæ¯\n\ndata: æµ‹è¯•ä¸æ–‡ï¼š\n\n"
[CDP Network.getResponseBody()]
base64Encoded: false
body type: string
body: "data: ä½ å¥½ï¼Œè¿™æ˜¯ç¬¬ä¸€æ¡æ¶ˆæ¯\n\ndata: æµ‹è¯•ä¸æ–‡ï¼š\n\n"
Additional context
@pavelfeldman
I apologize, the original issue (microsoft/playwright-python#3023) followed a template and included the reproduction steps, so I did not fill in all the information fields. I have now reproduced the problem using TypeScript.
Environment
System:
OS: Windows 11 10.0.22621
CPU: (36) x64 Intel(R) Xeon(R) CPU E5-2696 v3 @ 2.30GHz
Memory: 6.21 GB / 31.91 GB
Binaries:
Node: 24.14.0 - C:\Program Files\nodejs\node.EXE
npm: 11.9.0 - C:\Program Files\nodejs\npm.CMD
IDEs:
VSCode: 1.111.0 - C:\Users\admin\AppData\Local\Programs\Microsoft VS Code\bin\code.CMD
Claude Code: 2.1.78 - C:\Users\admin\AppData\Roaming\npm\claude.CMD
Codex: 0.115.0 - C:\Users\admin\AppData\Roaming\npm\codex.CMD
Languages:
Bash: 5.1.16 - C:\Windows\system32\bash.EXE
npmPackages:
playwright: 1.54.0 => 1.54.0Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
browser-chromiumupstreamThis is a bug in something playwright depends on, like a browser.This is a bug in something playwright depends on, like a browser.