microsoft / microsoft/playwright
[Bug]: Chromium no-store Fetch stream reaches EOF but request fails with ERR_ABORTED
- Dominant language
- TypeScript
- Stars
- 96.3k
- Forks
- 6.5k
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 180
Description
### Version
1.63.0 (matching bundled full Chromium 153.0.8010.12). The same script also reproduces with playwright-core 1.62.1 / Chromium 151.0.7922.34.
### Steps to reproduce
Repository: https://github.com/etonedu2026/playwright-no-store-stream-repro
The repository must remain **private** under the account owner's policy; maintainers will not have access by default. I understand the template requests a repository. To make this report independently reproducible without access, the complete synthetic script and exact dependency are included below. It contains no application code or application data.
In an empty directory, save the following as `package.json`:
```json
{
"name": "playwright-no-store-stream-repro",
"private": true,
"type": "module",
"devDependencies": { "playwright-core": "1.63.0" }
}
```
Save the following as `repro.mjs`:
```js
import { createServer } from 'node:http';
import { chromium } from 'playwright-core';
const server = createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
if (url.pathname !== '/data') return res.end('synthetic reproduction');
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': url.searchParams.get('cache'),
});
res.write('{"ok":');
setTimeout(() => res.end('true}'), 20);
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
const browser = await chromium.launch({ headless: true, executablePath: chromium.executablePath() });
const results = [];
try {
for (const [cache, reader] of [['no-store', 'stream'], ['no-store', 'json'], ['no-cache', 'stream']]) {
for (let repeat = 0; repeat < 2; repeat++) {
const context = await browser.newContext();
try {
const page = await context.newPage();
await page.goto(origin);
const matches = request => new URL(request.url()).pathname === '/data';
const terminal = new Promise(resolve => {
page.on('requestfinished', request => {
if (matches(request)) resolve({ event: 'requestfinished' });
});
page.on('requestfailed', request => {
if (matches(request)) resolve({ event: 'requestfailed', failure: request.failure()?.errorText });
});
});
const headers = page.waitForResponse(response => matches(response.request()));
const application = page.evaluate(async ({ cache, reader }) => {
const response = await fetch(`/data?cache=${cache}`);
if (reader === 'json') return (await response.json()).ok;
const handle = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await handle.read();
if (done) break;
chunks.push(...value);
}
handle.releaseLock();
return JSON.parse(new TextDecoder().decode(new Uint8Array(chunks))).ok;
}, { cache, reader });
const response = await headers;
let hostBodyOk = false;
let hostError = null;
try { hostBodyOk = (await response.json()).ok === true; }
catch (error) {
hostError = error.message.includes('No data found for resource with given identifier')
? 'Network.getResponseBody: No data found for resource with given identifier'
: 'Other response body error';
}
results.push({ cache, reader, repeat, pageBodyOk: await application, hostBodyOk, hostError, terminal: await terminal });
} finally { await context.close(); }
}
}
console.log(JSON.stringify({ node: process.version, browser: browser.version(), results }, null, 2));
} finally {
await browser.close();
server.closeAllConnections();
await new Promise(resolve => server.close(resolve));
}
```
Run on Linux:
```sh
npm install --ignore-scripts --no-audit --no-fund --registry=https://registry.npmjs.org
export PLAYWRIGHT_BROWSERS_PATH="$(mktemp -d)"
npx playwright-core install chromium
timeout 30s node repro.mjs
```
The repository pins the same version and includes a lockfile, so a repository checkout can use `npm ci`.
### Expected behavior
All six responses are completely consumed by the page. For each, Playwright should emit `requestfinished`, and host-side `response.json()` should return `{"ok":true}`.
### Actual behavior
Both `no-store + stream` repetitions:
- Page reader reaches `done: true`, parses all 11 bytes and returns `ok: true`.
- Playwright emits `requestfailed` with `net::ERR_ABORTED`.
- Host `response.json()` fails with `Network.getResponseBody: No data found for resource with given identifier`.
Both `no-store + native response.json()` controls and both `no-cache + stream` controls emit `requestfinished` and allow host body reading. The same six-case result was observed on both versions listed above.
The process exits 0 after printing the diagnostic results; exit 0 is not a passing assertion.
### Additional context
This uses only a synthetic loopback Node HTTP server and Playwright. There is no AbortController, reader.cancel, request interception, response mocking, Service Worker, application framework, account or database. Each page/context is closed only after the page promise and original request terminal event have completed.
The server's 20 ms delay separates two writes; it is not a client-side sleep intended to fix or hide a failure. Full Chromium is launched using `chromium.executablePath()`. Changing to `no-cache` is only a diagnostic control, not a proposed workaround for sensitive responses that must retain `no-store`.
I have not established the upstream implementation cause or whether it belongs to Chromium or Playwright. No application workaround is being claimed.
### Environment
Manually allowlisted environment fields (no environment-variable dump):
- OS: Ubuntu 26.04 LTS, Linux x86_64 under WSL2
- Kernel: 6.18.33.1-microsoft-standard-WSL2
- Node: 22.23.2
- npm: 10.9.8
- playwright-core: 1.63.0, exact npm dependency
- Browser: bundled Chromium 153.0.8010.12, headless, full executable
Contributor guide
Research direction
Start by running the supplied repro.mjs with the package.json dependency and compare the six reported cases. Trace the requestfinished/requestfailed events and response body handling for the no-store stream case. Done means the failing case is understood and corrected so it emits requestfinished and permits host-side response.json() to return ok:true.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, typescript
- Domain
- testing-qa, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100