What this usually means
Route interception relies on exact URL pattern matching and correct handler registration timing. If the pattern doesn't match, or if the page navigates before registration, the handler is never invoked. Also, Playwright merges multiple route handlers for the same pattern — the last registered wins, which can cause confusion when tests share state.
The first ten minutes — establish facts before touching code.
- 1Add console.log inside the route handler to confirm invocation: `page.route('**/*', async route => { console.log('INTERCEPTED', route.request().url()); await route.continue(); });`
- 2Check the URL pattern: use `**/*` to catch all, then narrow down. Ensure no trailing slash mismatch.
- 3Verify that `page.route` is called BEFORE the navigation that triggers the request.
- 4Run with `PWDEBUG=1` to see route handlers in the Playwright Inspector.
- 5Check for multiple route registrations — call `page.unroute('**/*')` before registering to clear previous handlers.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchTest file: the exact line where `page.route()` is called relative to `page.goto()`.
- searchPlaywright trace viewer: look for 'Route' events and see if handler was invoked.
- searchConsole output: any `route.continue()` or `route.abort()` errors.
- searchBrowser DevTools Network tab: confirm request URL matches the pattern.
- searchPlaywright test runner logs with `--trace on` for full route timeline.
- searchShared setup files (e.g., playwright.config.ts) if route handlers are registered globally.
Practical causes, not theory. These are the things you will actually find.
- warningGlob pattern mismatch: `**/*api**` vs `**/api/**` — subtle differences.
- warningMissing `method` option: route('**/*', handler, { method: 'GET' }) only intercepts GET.
- warningPage navigates or reloads after route registration — handlers are bound to page instance, not persisted across navigations.
- warningMultiple route registrations for same pattern — last one wins, earlier ones overridden.
- warningRoute handler throws an exception — Playwright silently catches and continues the request without modification.
- warningUsing `page.route` inside a `beforeAll` but page is created fresh for each test.
Concrete fix directions. Pick the one that matches your root cause.
- buildUse `page.route('**/*')` as a catch-all first to verify interception works, then narrow pattern.
- buildAdd `await page.unroute('**/*')` before each test's route registration to remove stale handlers.
- buildRegister routes inside the test block after page creation, not in global setup.
- buildWrap route handler body in try-catch and log errors to stderr.
- buildIf intercepting specific API, use `page.waitForResponse` instead to verify interception happened.
- buildFor POST requests, explicitly set `method: 'POST'` in route options.
A fix you cannot prove is a guess. Close the loop.
- verifiedAdd a unique header to the intercepted response and assert its presence in the page.
- verifiedUse `page.on('request', req => console.log(req.url()))` to list all requests and compare against route pattern.
- verifiedCheck Playwright trace viewer: open trace, go to 'Network' tab, confirm 'Route' column shows handled.
- verifiedWrite a unit test that intercepts a request and asserts `route.request().url()` matches expected.
- verifiedTemporarily change route handler to `route.abort('blockedbyclient')` and verify the request is blocked.
Things that make this bug worse or harder to find.
- warningDon't assume glob patterns work like regex — `*` does not match `/` in path segments.
- warningNever register routes inside `page.waitForURL` or after `page.goto` — the navigation may complete before handler is active.
- warningAvoid sharing page objects across tests without clearing routes — state leaks.
- warningDon't forget `await route.continue()` or `route.fulfill()` — missing it causes request to hang.
- warningDon't use `page.route` with arrow functions that capture stale variables inside loops.
- warningDon't rely on `page.route` for WebSocket traffic — use `page.routeWebSocket` instead.
Mocked API Not Intercepted — Silent Failure in CI
Timeline
- 09:15Wrote test to intercept `/api/users` and return mock data.
- 09:20Test passes locally, fails in CI — mock data not applied.
- 09:30Added console.log in route handler — never fires in CI.
- 09:45Checked CI logs: page.goto URL had trailing slash, pattern was `/api/users` no slash.
- 10:00Fixed pattern to `/api/users**` — still not working.
- 10:15Noticed CI runs tests in parallel, page is reused — route handler overridden.
- 10:20Added `page.unroute` before each test route registration.
- 10:25Test passed in CI.
We had a test that intercepted GET /api/users to return a mock user list. Locally it worked flawlessly. In CI, the test would fail because the page showed real data. I started by adding a console.log inside the route handler — it never appeared in CI logs. That told me the handler wasn't being called.
I checked the URL pattern. Our page.goto was `https://app.example.com/users/` with a trailing slash, but my route pattern was `/api/users` without. I changed it to `/api/users**` to match any path starting with /api/users. Still no luck.
Then I realized: CI runs tests in parallel with shared page instances. Another test had registered a route with the same pattern earlier, overriding mine. I added `await page.unroute('**/api/users**')` right before my route registration. That fixed it. The lesson: route handlers are per-page and last registration wins, so always unroute before registering in shared environments.
Root cause
Route handler overridden by earlier test registration due to page reuse, and URL pattern mismatch with trailing slash.
The fix
Call `page.unroute('**/api/users**')` before `page.route('**/api/users**', handler)` and ensure pattern matches actual request URL including any trailing slash.
The lesson
Always unroute before route in setup, and use `page.waitForResponse` to verify interception occurred.
Playwright uses a glob matching syntax, not regex. `*` matches zero or more characters except `/`. So `**/api/*` matches `/api/users` but not `/api/v1/users`. To match any depth, use `**/api/**`.
Trailing slashes matter: `https://example.com/api/` does not match pattern `*/api` because the path is `/api/` not `/api`. Always include `**` at the end if you want to match any suffix.
Query strings are part of the URL for pattern matching. Pattern `*/api/users` does NOT match `/api/users?page=1`. Use `*/api/users**` to include query parameters.
When multiple handlers are registered for the same pattern, the last one registered wins for that pattern. If you register a broad pattern like `**/*` first, then a specific pattern like `**/api/users`, the specific one will be used for matching requests.
However, if you register the specific pattern first, then the broad pattern, the broad pattern will override the specific one for all requests. This is a common source of bugs.
The safe approach: register all routes in order from most specific to most broad, or use `page.unroute` before each registration to ensure only one handler exists.
Route handlers are bound to a page instance. If the page navigates to a different origin or reloads, the handlers remain attached. But if you close the page and create a new one, handlers are lost.
In test frameworks like Playwright Test, each test typically gets a fresh page. If you register routes in `beforeAll`, they apply to the page created in the first test, not subsequent tests. Always register routes inside the test or in `beforeEach`.
When using `page.route` after `page.goto`, there is a race condition: the navigation may have already started and the request may have been sent before the handler is registered. Always register routes before any navigation.
Run tests with `PWDEBUG=1` to open Playwright Inspector. In the Inspector, you can see all registered routes under the 'Route' tab. It shows the pattern, handler status, and whether it was invoked.
Use the trace viewer: after a test run, open the trace file. Go to the 'Network' tab. Each request shows a 'Route' column — if it was intercepted, it shows 'handled' along with the handler name.
You can also use `page.on('route', route => console.log('Route triggered:', route.request().url()))` to log every time a route handler is about to be called.
Forgetting `await` on `route.continue()` or `route.fulfill()` is a common mistake. The request will hang until the promise resolves, but if the promise is not awaited, the test may continue and fail due to timeout.
If you have multiple asynchronous operations inside the handler, ensure all are awaited. Use `route.fulfill({ body: JSON.stringify(data) })` with await.
If you're using `route.abort()`, it returns immediately, but still good practice to await for consistency.
Frequently asked questions
Why does route.intercept work locally but fail in CI?
Common causes: different URL patterns (e.g., trailing slash), page reuse across tests (handler overridden), or environment variables affecting the request URL. Use `unroute` before each registration and add logging to confirm handler invocation.
How do I intercept all requests on a page?
Use `page.route('**/*', handler)`. The pattern `**/*` matches any URL. But be careful: this intercepts every request including images, styles, and fonts. Filter by URL inside the handler if needed.
Can I intercept requests after page has loaded?
Yes, but only for requests made after the handler is registered. Requests that already started before registration won't be intercepted. Register routes before any navigation or before the action that triggers the request.
What's the difference between route.fulfill and route.continue?
`route.fulfill()` sends a mock response directly without contacting the server. `route.continue()` sends the request to the server but allows you to modify headers or post data via options. Use fulfill for mocking, continue for modifying outgoing requests.
How do I intercept requests from iframes?
Use `frame.route()` on the specific frame. If you only have the page, you can use `page.route()` which intercepts requests from all frames. Note that the request's `frame()` property tells you which frame initiated it.