Problem
setRequestInterception(false) kills every route registered afterwards, silently.
const page = new Browser().newPage()
page.route('https://a.test/**', route => route.fulfill({ status: 200, body: 'first' }))
await (await fetch('https://a.test/x')).text() // 'first' — works
await page.setRequestInterception(false)
page.route('https://a.test/**', route => route.fulfill({ status: 200, body: 'second' }))
await fetch('https://a.test/y')
// ECONNREFUSED — the route never ran and the request went to the real networkTwo pieces of state disagree. setRequestInterception(false) calls
_requestInterceptor.clear() (BrowserPage.ts:1112), which drops every
handler including the one _installRouting added to run the routes. But
_routingInstalled stays true, and _installRouting returns early on it:
private _installRouting(): void {
if (this._routingInstalled)
return // <- nothing is reinstalled, ever
this._routingInstalled = true
this._requestInterceptor.enable()
this._requestInterceptor.addHandler(async request => { await this._runRoutes(request) })
}So route() goes on accepting handlers, storing them in _routes, and
returning normally. Nothing ever consults them again.
Why it matters
The failure is silent in the worst way: the request is not blocked, it escapes. A test that meant to mock an endpoint reaches the real one instead. Under CI that is a network error from a host the test was never supposed to contact; on a developer's machine with network access it may quietly pass against live data.
route() returning normally is what makes it hard to find. There is no error at
the registration, and the eventual failure surfaces as a connection error
pointing at fetch, with nothing naming the route that was supposed to answer.
The sequence is not exotic. A shared teardown that calls both —
afterEach(async () => {
await page.unroute()
await page.setRequestInterception(false)
})— is the obvious way to keep one file's routes out of another's, since both are
installed on the global fetch. It leaves the page unable to route for the rest
of its life.
Proposed scope
Decide which of the two is the bug and fix that one, rather than patching both:
- Preferred:
clear()should not be reachable fromsetRequestInterception(false), because routes and explicit interception are separate features that happen to share a mechanism. Disabling one should not dismantle the other. If the intent was "forget the handlers I added", it should remove only those. - Failing that, reset
_routingInstalled = falsewherever the handler can be dropped, so the nextroute()reinstalls it. This is the smaller change and leaves the coupling in place.
Note that reporting is no longer affected: request and response events moved
onto interceptor observers, which clear() deliberately leaves alone (#1606).
This issue is only about the routing handler.
While here: unroute() with no arguments empties _routes but leaves
interception enabled and the routing handler installed, which is harmless but
worth a look for symmetry.
Acceptance
- The reproduction above serves
'second'. - A test asserts that a route registered after
setRequestInterception(false)still intercepts — the guard this lacked. setRequestInterception(false)still stops explicit interception, asserted, so the fix does not simply make the disable a no-op.- Routes registered before the disable are unaffected either way.
Found while adding waitForRequest (#1606): the teardown in
test/page-event-waiters.test.ts uses exactly this sequence, and it only goes
unnoticed there because each test builds a fresh page.