Summary
Request interception exists in the Puppeteer shape — page.on('request', …) plus await page.setRequestInterception(true) — but Playwright's route() is missing on both BrowserPage and BrowserContext.
The difference is not just naming. route() takes a URL pattern, so handlers are scoped to the requests they care about and unmatched requests are untouched. With a single request handler, every handler sees every request and each one must pattern-match by hand and remember to call continue(), or the request hangs.
Reproduction
import { Browser } from 'very-happy-dom'
const page = new Browser().newPage()
console.log(typeof page.route) // undefined
console.log(typeof page.unroute) // undefined
console.log(typeof page.context.route) // undefined
console.log(typeof page.setRequestInterception) // functionThe existing path works, for reference:
const page = new Browser().newPage()
page.on('request', (request) => {
request.respond({ status: 200, body: 'mocked' })
})
await page.setRequestInterception(true)
const response = await fetch('https://example.test/api/x')
console.log(response.status, await response.text()) // 200 mockedConfirmed against main at 5fae54d4 with Bun 1.4.3.
Current implementation
BrowserPage.setRequestInterception() enables a RequestInterceptor that replaces global fetch and fans every request out to the page's request listeners. InterceptedRequest already carries the three terminal actions (continue, respond, abort), so the per-request half of the model is in place — what is missing is pattern-based registration.
Expected behavior
await page.route('**/api/**', async (route) => {
await route.fulfill({ status: 200, body: JSON.stringify({ mocked: true }) })
})
await page.route(/\.png$/, route => route.abort())
await page.unroute('**/api/**')Requirements:
route(url, handler)accepts a glob string, aRegExp, or a predicate function- enabling interception is implicit — registering a route is enough, with no separate
setRequestInterception(true)call - requests matching no route pass through untouched
- when several routes match, the most recently registered wins, and
route.fallback()hands off to the next unroute(url, handler?)removes one or all handlers for a pattern- the same API on
BrowserContext, applying to every page in it
Route object, mapping onto what InterceptedRequest already does:
| Method | Existing equivalent |
|---|---|
route.fulfill({ status, headers, body, json, path, contentType }) | respond() |
route.abort(errorCode?) | abort() |
route.continue({ url, method, headers, postData }) | continue() |
route.fetch() | — performs the request and returns the response for rewriting |
route.request() | — the Request being intercepted |
Worth keeping setRequestInterception and page.on('request') working alongside it, since they are the happy-dom/Puppeteer-compatible surface and removing them would be a breaking change.
Also note the interceptor currently swaps global fetch, so two pages intercepting at once will contend for it. Per-page or per-context routing needs the interception to be scoped rather than global.