microsoft / microsoft/playwright

[Bug]: Chromium no-store Fetch stream reaches EOF but request fails with ERR_ABORTED

Open
#42,742 2 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.