Repository navigation
[Regression]: Chromium click on a link answered with 204 No Content times out (1.64) #43279
Description
Version
1.64.0, and 1.65.0-alpha-2026-10-11. Last good: 1.63.0.
Steps to reproduce
Save as repro.mjs and run with node repro.mjs:
import http from 'node:http';
import { chromium } from 'playwright';
const server = http.createServer((req, res) => {
if (req.url === '/nothing') { res.writeHead(204); return res.end(); }
res.writeHead(200, { 'content-type': 'text/html' });
res.end(req.url === '/framed'
? '<iframe src="/frame"></iframe>'
: '<a id="l" href="/nothing">204 link</a>');
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
const browser = await chromium.launch();
const page = await browser.newPage();
const time = async (name, fn) => {
const start = Date.now();
try { await fn(); console.log(name, 'ok', Date.now() - start, 'ms'); }
catch (e) { console.log(name, 'FAIL', Date.now() - start, 'ms', e.message.split('\n')[0]); }
};
await page.goto(base + '/');
await time('click', () => page.click('#l', { timeout: 8000 }));
await page.goto(base + '/');
await time('click, noWaitAfter', () => page.click('#l', { timeout: 8000, noWaitAfter: true }));
await page.goto(base + '/framed');
await time('click in iframe', () => page.frameLocator('iframe').locator('#l').click({ timeout: 8000 }));
await browser.close();
server.close();Expected behavior
The click resolves right away, as in 1.63. A navigation answered with 204 No Content is dropped by the browser and never commits, so there is nothing to wait for.
Actual behavior
On Chromium, the top-level click waits until the action's timeout and then throws page.click: Timeout 8000ms exceeded.
| Chromium | Firefox | WebKit | |
|---|---|---|---|
| 1.63.0 | ok, 64 ms | ||
| 1.64.0 | FAIL, timeout | ok, 149 ms | ok, 176 ms |
1.64.0, noWaitAfter: true |
ok, 57 ms | ok, 92 ms | ok, 52 ms |
| 1.64.0, the same link inside an iframe | ok, 84 ms | ok, 115 ms | ok, 86 ms |
| 1.65.0-alpha-2026-10-11 | FAIL, timeout |
It is not the Chromium roll. With channel: 'chrome' against the same installed Google Chrome 154.0.8037.98, 1.63.0 passes in 50 ms and 1.64.0 times out. The change looks to be in how the Chromium driver's after-action wait treats a navigation that is requested but never committed.
Bisected to 1.64.0-alpha-2026-09-22 (last good: 1.64.0-alpha-2026-09-21), i.e. #42834, "report 204 subresource responses as finished" (the fix for #42786). That change also reports a 204 navigation as finished, so the frame's pending navigation is never aborted.
Additional context
It turned up in a tool that clicks links in arbitrary web apps. Links that answer 204 (tracking pixels, "mark as read" links, download endpoints with nothing to send) are common, and on 1.64 every such click now costs the full action timeout.
A likely fix: keep #42834's 204-as-finished path for subresources only, in packages/playwright-core/src/server/chromium/crNetworkManager.ts:
if (response?.status() === 204 && !request.request.isNavigationRequest()) {A 204 navigation is then reported as failed again, as in 1.63 and as Firefox and WebKit report it, so the pending navigation is aborted. Locally, with this change, a click test and a requestfailed test for a 204 navigation pass on all three browsers.
Environment
macOS 26 (Darwin 25.6.0), arm64
Node.js 24.11.1
playwright 1.64.0 (bundled Chromium 156.0.8078.4); also 1.65.0-alpha-2026-10-11 (Chromium 156.0.8078.12)
Google Chrome 154.0.8037.98 (channel: 'chrome')
Activity