From 7b98cc350f8ef7bd56bda309b5aff5664dd6fed5 Mon Sep 17 00:00:00 2001 From: Gil Cohen <33692883+Gilc83@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:30:52 +0300 Subject: [PATCH] fix(playwright): silence transient errors in `enqueueLinksByClickingElements` (#3732) `enqueueLinksByClickingElements` installs a context-wide interceptor (`context.route('**', onInterceptedRequest)`); the handler calls `isTopFrameNavigationRequest` -> `req.frame()`, which **throws** when the owning frame is unavailable (request issued by a service worker, or before/after the frame exists). The throw escapes the async `route` handler, so the route is never `continue()`/`abort()`ed: that request hangs and can abort the whole click-and-intercept pass. Reproduces on PWAs and on pages where clicking detaches frames (galleries, paginated forums). ### Fix Wrap the check in `try/catch`; a throw means "not a top-frame navigation", so return `false` and let the request `continue()` normally instead of crashing the route handler. ### Secondary fix (same function) The interceptor is registered with `context.route('**', ...)` but torn down with `context.unroute('*', ...)`. Playwright matches `unroute` by the same URL glob, and `'*' !== '**'`, so the handler is not actually removed - leaking an interceptor across page reuses. Changed to `unroute('**', ...)`. Fixes #3216. Signed-off-by: Gil Cohen Co-authored-by: Claude Opus 4.8 --- .../src/internals/enqueue-links/click-elements.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/playwright-crawler/src/internals/enqueue-links/click-elements.ts b/packages/playwright-crawler/src/internals/enqueue-links/click-elements.ts index bd5acf278a27..78e7c4059266 100644 --- a/packages/playwright-crawler/src/internals/enqueue-links/click-elements.ts +++ b/packages/playwright-crawler/src/internals/enqueue-links/click-elements.ts @@ -352,7 +352,7 @@ export async function clickElementsAndInterceptNavigationRequests( // browser.off(BrowserEmittedEvents.TargetCreated, onTargetCreated); page.off('framenavigated', onFrameNavigated); - await context.unroute('*', onInterceptedRequest); + await context.unroute('**', onInterceptedRequest); const serializedRequests = Array.from(uniqueRequests); return serializedRequests.map((r) => JSON.parse(r)); @@ -405,7 +405,15 @@ function createTargetCreatedHandler(requests: Set): (popup: Page) => Pro * @ignore */ function isTopFrameNavigationRequest(page: Page, req: Request): boolean { - return req.isNavigationRequest() && req.frame() === page.mainFrame(); + try { + return req.isNavigationRequest() && req.frame() === page.mainFrame(); + } catch { + // `req.frame()` throws when the owning frame is unavailable - e.g. the request was + // issued by a service worker, or before/after its frame existed (see #3216). Such a + // request is not a top-frame navigation, so swallow the throw and let it pass through + // instead of crashing the route handler (which would leave the route unhandled). + return false; + } } /**