← Back to microsoft/playwright
microsoft / playwright · Issue No. 42862
Bisected while investigating a CI break from a routine @playwright/test 1.62.1 → 1.63.0 bump in apache/superset (Dependabot apache/superset#44231). Several e2e tests that assert on a downloaded file's contents via page.waitForResponse(...) + response.body() started failing with a 0-byte body, while the app itself (and the in-page blob.size) was completely fine.
A page-level Response.body() (captured via page.waitForResponse()) returns an empty buffer if the in-page JavaScript has already consumed the same fetch() Response's body (e.g. via .blob(), as any real download-triggering code does: const blob = await response.blob()).
response.body() works correctly on both 1.62.1 and 1.63.0..blob(), response.body() returns 0 bytes on 1.63.0, while the browser itself received the full body fine (blob.size is correct) - so this looks like a regression in how Playwright's own CDP-based body capture interacts with a body stream the page has also read, not a real network/browser issue.Self-contained, only needs @playwright/test installed for chromium:
// standalone-repro.mjs
import http from 'node:http';
import { chromium } from '@playwright/test';
const body = Buffer.alloc(50000, 0x50);
const server = http.createServer((req, res) => {
if (req.url === '/blank') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`<html><body>
<button id="go">go</button>
<script>
document.getElementById('go').addEventListener('click', async () => {
const response = await fetch('/export');
// Consuming the body in-page (as any real app does with a
// downloaded file) is what triggers the regression.
const blob = await response.blob();
window.__blobSize = blob.size;
});
</script>
</body></html>`);
return;
}
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Length': String(body.length),
});
res.end(body);
});
await new Promise(resolve => server.listen(8899, resolve));
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:8899/blank');
const responsePromise = page.waitForResponse(res => res.url().includes('/export'));
await page.click('#go');
const response = await responsePromise;
const nodeBody = await response.body();
const inPageBlobSize = await page.evaluate(() => window.__blobSize);
console.log('expected byte length:', body.length);
console.log('in-page blob.size (browser correctly received it):', inPageBlobSize);
console.log('response.body() on the Node side:', nodeBody.length);
await browser.close();
server.close();
if (nodeBody.length !== body.length) {
console.error('BUG: response.body() did not match the actual response body.');
process.exit(1);
}
console.log('OK');
@playwright/test@1.62.1:
expected byte length: 50000
in-page blob.size (browser correctly received it): 50000
response.body() on the Node side: 50000
OK
@playwright/test@1.63.0:
expected byte length: 50000
in-page blob.size (browser correctly received it): 50000
response.body() on the Node side: 0
BUG: response.body() did not match the actual response body.
Bisected with npm install @playwright/test@1.62.1 vs @1.63.0 in two isolated directories, otherwise identical repro and chromium build for that version. Reproduces consistently (not flaky) on both macOS 27.0 (arm64) and Ubuntu 24.04 (the CI environment where it originally surfaced).
chromium.launch(), default channel)ubuntu-24.04)Any test that clicks a real download/export action and then asserts on the fetched file's contents via page.waitForResponse() + response.body() will silently see an empty body once the app's own code has read the response (which is the normal way to turn a fetch() response into a downloadable file). The actual application behavior is unaffected - this only breaks the test's ability to observe what the browser already correctly received.
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.