From 99275360242334053c0213aab9ba5e2eb56e6731 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 10 Oct 2026 09:53:27 -0400 Subject: [PATCH 1/2] Follow navigation redirects with Playwright 1.64 From 1.64, an action whose navigation ends in the redirect's 204 never settles. --- bin/lib/core.js | 14 ++++++++++++-- bin/playwright-server.js | 5 +++-- tests/Functional/Network/RouteTest.php | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/bin/lib/core.js b/bin/lib/core.js index ae77ed7..b34aa36 100644 --- a/bin/lib/core.js +++ b/bin/lib/core.js @@ -167,6 +167,15 @@ class CommandRegistry { } } +class NavigationRedirects extends Map { + #waiters = new Map(); + fulfilled(pageId) { return new Promise(resolve => this.#waiters.set(pageId, resolve)); } + markFulfilled(pageId) { + this.#waiters.get(pageId)?.(); + this.#waiters.delete(pageId); + } +} + class BaseHandler { constructor(deps = {}) { Object.assign(this, deps); } validateResource(resourceMap, resourceId, resourceType) { @@ -199,7 +208,8 @@ class BaseHandler { this.navigationRedirects.delete(pageId); try { - result = await nextAction(); + // from Playwright 1.64, an action whose navigation ends in the redirect's 204 never settles + result = await Promise.race([nextAction(), this.navigationRedirects.fulfilled(pageId)]); } catch (error) { if (!this.navigationRedirects.has(pageId)) throw error; } @@ -330,5 +340,5 @@ const logger = new Logger(); module.exports = { logger, ErrorHandler, LspFraming, sendFramedResponse, - CommandRegistry, BaseHandler, PromiseUtils, FrameUtils, RouteUtils, + CommandRegistry, NavigationRedirects, BaseHandler, PromiseUtils, FrameUtils, RouteUtils, }; diff --git a/bin/playwright-server.js b/bin/playwright-server.js index 05fde22..561abf6 100644 --- a/bin/playwright-server.js +++ b/bin/playwright-server.js @@ -1,5 +1,5 @@ const {chromium, firefox, webkit, request} = require('playwright'); -const { logger, ErrorHandler, LspFraming, sendFramedResponse, CommandRegistry, BaseHandler } = require('./lib/core'); +const { logger, ErrorHandler, LspFraming, sendFramedResponse, CommandRegistry, NavigationRedirects, BaseHandler } = require('./lib/core'); const { APIRequestHandler, ContextHandler, PageHandler, LocatorHandler, InteractionHandler, FrameHandler, JSHandleHandler, SelectorsHandler, VideoHandler } = require('./lib/handlers'); const { globalCoordinator } = require('./lib/coordination'); @@ -20,7 +20,7 @@ class PlaywrightServer extends BaseHandler { this.dialogs = new Map(); this.elementHandles = new Map(); this.contextThrottling = new Map(); - this.navigationRedirects = new Map(); + this.navigationRedirects = new NavigationRedirects(); this.servers = new Map(); this.videos = new Map(); this.apiContexts = new Map(); @@ -148,6 +148,7 @@ class PlaywrightServer extends BaseHandler { if (!this.pages.has(info.contextId)) throw new Error('Navigation redirects require a page route'); this.navigationRedirects.set(info.contextId, command.url); await route.fulfill({ status: 204, headers: command.options?.headers }); + this.navigationRedirects.markFulfilled(info.contextId); } async continueRoute(route, info, command) { diff --git a/tests/Functional/Network/RouteTest.php b/tests/Functional/Network/RouteTest.php index 382850e..ea43baa 100644 --- a/tests/Functional/Network/RouteTest.php +++ b/tests/Functional/Network/RouteTest.php @@ -221,6 +221,20 @@ public function testCanRedirectNavigationRequestAfterClick(): void $this->assertSame($baseUrl.'/page-2.html', $this->page->url()); } + public function testCanRedirectNavigationRequestAfterFormSubmit(): void + { + $baseUrl = $this->getBaseUrl(); + $this->goto('/index.html'); + $this->page->evaluate("document.body.innerHTML = '
'"); + $this->page->route('**/login', static function (Route $route) use ($baseUrl): void { + $route->redirectNavigationRequest($baseUrl.'/page-2.html'); + }); + + $this->page->locator('button')->click(); + + $this->assertSame($baseUrl.'/page-2.html', $this->page->url()); + } + public function testCanRedirectNavigationRequestAfterReload(): void { $baseUrl = $this->getBaseUrl(); From aeaecc03b961830c28e31f96763049c96f0e8bd2 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 10 Oct 2026 10:20:04 -0400 Subject: [PATCH 2/2] Wait for `goto()` to settle between navigation redirects Racing it let a `goto()` to the same url start before the previous one settled, which fails with `net::ERR_ABORTED`. --- bin/lib/core.js | 9 ++++++--- bin/lib/handlers.js | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bin/lib/core.js b/bin/lib/core.js index b34aa36..641d0a3 100644 --- a/bin/lib/core.js +++ b/bin/lib/core.js @@ -199,7 +199,7 @@ class BaseHandler { return { handleId }; } async executeWithRegistry(registry, method, ...args) { return await registry.execute(method, ...args); } - async followNavigationRedirects(pageId, page, action) { + async followNavigationRedirects(pageId, page, action, settlesOnRedirect = false) { let nextAction = action; let redirectCount = 0; let result; @@ -208,8 +208,10 @@ class BaseHandler { this.navigationRedirects.delete(pageId); try { - // from Playwright 1.64, an action whose navigation ends in the redirect's 204 never settles - result = await Promise.race([nextAction(), this.navigationRedirects.fulfilled(pageId)]); + // from Playwright 1.64 only goto() settles on the redirect's 204; a goto() to the same url before it does fails with net::ERR_ABORTED + result = await (settlesOnRedirect + ? nextAction() + : Promise.race([nextAction(), this.navigationRedirects.fulfilled(pageId)])); } catch (error) { if (!this.navigationRedirects.has(pageId)) throw error; } @@ -222,6 +224,7 @@ class BaseHandler { throw new Error(`Too many navigation redirects for page ${pageId}`); } nextAction = () => page.goto(target); + settlesOnRedirect = true; } } } diff --git a/bin/lib/handlers.js b/bin/lib/handlers.js index 6210335..68f6016 100644 --- a/bin/lib/handlers.js +++ b/bin/lib/handlers.js @@ -440,7 +440,8 @@ class PageHandler extends BaseHandler { const gotoResponse = await this.followNavigationRedirects( command.pageId, page, - () => page.goto(command.url, command.options) + () => page.goto(command.url, command.options), + true ); return { response: this.serializeResponse(gotoResponse) }; }