Repository navigation
Description
Version
1.64.1 (also reproduced on 1.60.0)
Steps to reproduce
- In an empty directory:
npm init -y && npm pkg set type=module && npm i @playwright/test@1.64.1 - Save this as
repro.mjsand runnode repro.mjs. It uses the installed Chrome; setCHANNEL=chromiumto use Playwright's Chromium instead.
import { chromium } from '@playwright/test'
const browser = await chromium.launch({ channel: process.env.CHANNEL || 'chrome' })
const context = await browser.newContext()
const page = await context.newPage()
const delays = { '/a': 50, '/b': 400 }
let started = 0
await context.route('**/*', async (route) => {
const url = new URL(route.request().url())
if (url.pathname === '/') return route.fulfill({ body: '<p>ok</p>', contentType: 'text/html' })
started++
await new Promise((resolve) => setTimeout(resolve, delays[url.pathname]))
await route.fulfill({ body: url.pathname }).then(
() => console.log('fulfilled', url.pathname),
(error) => console.log('fulfill failed', url.pathname, error.message),
)
})
context.on('requestfailed', (request) =>
console.log('request failed', new URL(request.url()).pathname, request.failure()?.errorText),
)
await page.goto('http://repro.test/')
void page.evaluate(() => Promise.allSettled(['/a', '/b'].map((path) => fetch(path))))
while (started < 2) await new Promise((resolve) => setTimeout(resolve, 5))
console.log('unrouteAll with /a and /b in flight')
const result = await Promise.race([
context.unrouteAll({ behavior: 'wait' }).then(() => 'unrouteAll resolved'),
new Promise((resolve) => setTimeout(() => resolve('unrouteAll still pending after 3s'), 3000)),
])
console.log(result)
await browser.close()
Expected behavior
unrouteAll({ behavior: 'wait' }) waits for both in-flight handlers, both fulfills succeed, and unrouteAll resolves:
unrouteAll with /a and /b in flight
fulfilled /a
fulfilled /b
unrouteAll resolved
Actual behavior
unrouteAll with /a and /b in flight
fulfilled /a
request failed /b net::ERR_NAME_NOT_RESOLVED
fulfill failed /b route.fulfill: Route is already handled!
unrouteAll still pending after 3s
As soon as the first handler (/a) finishes, /b is sent to the real network while its handler is still running. Its later route.fulfill throws Route is already handled!. If the handler lets that error propagate, it surfaces as an unhandled rejection (in the test runner, a failed test). If the handler catches it, as above, unrouteAll never resolves.
Additional context
This looks like a remaining multi-handler case of #36585, which #36588 fixed for a single in-flight handler. In _onRoute (both the page and the context versions), after a handler returns:
const handled = await routeHandler.handle(route);
if (!this._routes.length)
this._updateInterceptionPatterns({ internal: true }).catch(() => {});
unrouteAll has already emptied _routes, so the first handler to finish sends setNetworkInterceptionPatterns with no patterns while _unrouteInternal is still awaiting stop('wait') on the others. The server then drops the interceptor and continues the remaining intercepted requests, so their handlers can no longer fulfill them.
A possible fix: skip that update while a behavior: 'wait' unroute is pending, and let _unrouteInternal update the patterns once every removed handler has stopped (it already does).
Workaround: track your own in-flight handlers, wait for them to settle, then close the pages instead of calling unrouteAll.
We hit this in CI as an intermittent Route is already handled! failure during fixture teardown that called context.unrouteAll({ behavior: 'wait' }) while two mocked requests were in flight.
Environment
OS: macOS 26.3.1 (Darwin arm64); also seen on ubuntu-latest in GitHub Actions
Node.js: 22.23.0
@playwright/test: 1.64.1 and 1.60.0
Browser: Chrome 156.0.8078.80 (channel: 'chrome'); CI used chromium-headless-shell
Activity
darshpatel1052 commentedon Nov 3, 2026
I’d like to work on this. I’d add a regression with two delayed in-flight routes, then trace when page and context handlers update interception patterns during
unrouteAll({ behavior: 'wait' }). I’d make sure both handlers finish before interception is removed, while checking the other unroute behaviours. Is there a preferred test location for covering both page and context variants?github-actions commentedon Nov 3, 2026
microsoft-playwright-automation commentedon Nov 3, 2026
Hi, I'm the Playwright bot. I reproduced this on Linux.
Reproduced on current main (
1.65.0-next,3cd8a42c5ee15e559c193f0face72d237af0fc72) in Chromium, Firefox and WebKit, for both page and context routes. The first completed handler disables interception whileunrouteAll({ behavior: 'wait' })is still waiting for the other handler. The proposed fix leaves that update tounrouteAll()when it has replaced the route list.Reproduction and verification
Extended existing tests RO-200 and RO-201 in
tests/library/unroute-behavior.spec.ts. This is the context case; the page case usespage.route()andpage.unrouteAll():ManualPromisecomes fromisoinpackages/playwright-core/lib/coreBundle;page,contextandserverare the standard library-test fixtures.Linux, headless: Chromium 158.0.8127.2 (build 1253), Firefox 159.0 (build 1572), WebKit 27.2 (build 2388). Both cases failed on all three browsers without the fix: the second response came from the server instead of the mock, and its handler threw
Route is already handled!. All six cases pass with the fix. No browser-specific difference was observed.Commands:
The regression command failed in all six cases without the fix. Build and lint passed with the fix. The final four-file run passed: 325 passed, 8 existing skips.
Draft PR: #43889
Triaged on Linux by the Playwright bot - agent run
darshpatel1052 commentedon Nov 4, 2026
Thanks, I see this is fixed in #43889.