From 6df1dfe2380208bba33ca845aea8481cc6bbe5e9 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Fri, 22 May 2026 15:41:15 +0200 Subject: [PATCH] Replace active page on synthetic root navigation (about:blank, blob:) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page.navigate("about:blank") (and blob:) issued against a non-blank tab routed through Session.initiateRootNavigation, which always allocated a pending Page. A pending Page is promoted to active only by frameHeaderDoneCallback when HTTP response headers arrive — but synthetic navigations ZIGFLAGS= make no HTTP request, so the pending Page was never committed and the previous document stayed active (window.location.href, document.URL and Page.getFrameTree all kept reporting the old page). Route synthetic URLs through the existing immediate-swap path (replaceRootImmediate) from initiateRootNavigation, mirroring what processRootQueuedNavigation already does for JS-initiated synthetic navigations. replaceRootImmediate now takes (frame_id, url, opts) so both call sites share it. Fixes #2363 --- src/browser/Session.zig | 29 +++++++++++++++++++---------- src/cdp/domains/page.zig | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/browser/Session.zig b/src/browser/Session.zig index f388b0fb..1b585f96 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -489,7 +489,8 @@ fn processRootQueuedNavigation(self: *Session) !void { const is_synthetic = qn.is_about_blank or std.mem.startsWith(u8, qn.url, "blob:"); if (is_synthetic) { - return self.replaceRootImmediate(current_frame._frame_id, qn); + defer self.arena_pool.release(qn.arena); + return self.replaceRootImmediate(current_frame._frame_id, qn.url, qn.opts); } // The qn arena is consumed here regardless of success — frame.navigate @@ -500,18 +501,18 @@ fn processRootQueuedNavigation(self: *Session) !void { return self.initiateRootNavigation(current_frame._frame_id, qn.url, qn.opts); } -// Legacy immediate-swap path: tear down the active page and create a new one -// in its place before issuing the navigation. Used for synthetic navigations -// (about:blank, blob:) where there is no in-flight HTTP and therefore no -// "pending" window to span. -fn replaceRootImmediate(self: *Session, frame_id: u32, qn: *QueuedNavigation) !void { - defer self.arena_pool.release(qn.arena); - +// Immediate-swap path for synthetic navigations (about:blank, blob:): there is +// no in-flight HTTP and therefore no "pending" window to span — and no +// frameHeaderDoneCallback to commit a pending Page. Tear down the active page +// and create a new one in its place, then navigate it. Reached from both the +// queued-navigation path (processRootQueuedNavigation) and the CDP entry point +// (initiateRootNavigation); each caller owns any arena tied to `url`/`opts`. +fn replaceRootImmediate(self: *Session, frame_id: u32, url: [:0]const u8, opts: Frame.NavigateOpts) !void { self.tearDownActivePage(); const new_frame = try self.installNewActivePage(frame_id); - new_frame.navigate(qn.url, qn.opts) catch |err| { - log.err(.browser, "queued navigation error", .{ .err = err }); + new_frame.navigate(url, opts) catch |err| { + log.err(.browser, "synthetic navigation error", .{ .err = err, .url = url }); return err; }; } @@ -524,6 +525,14 @@ fn replaceRootImmediate(self: *Session, frame_id: u32, qn: *QueuedNavigation) !v pub fn initiateRootNavigation(self: *Session, frame_id: u32, url: [:0]const u8, opts: Frame.NavigateOpts) !void { self.discardPendingPage(); + // Synthetic navigations (about:blank, blob:) have no HTTP round-trip and + // therefore no frameHeaderDoneCallback to commit a pending Page. Swap the + // active Page immediately instead of allocating a pending one that would + // never be promoted, leaving the previous document in place (issue #2363). + if (std.mem.eql(u8, "about:blank", url) or std.mem.startsWith(u8, url, "blob:")) { + return self.replaceRootImmediate(frame_id, url, opts); + } + const page = try self.allocatePage(frame_id); errdefer self.destroyPage(page); diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index bb0274e1..f84e496f 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -1309,6 +1309,42 @@ test "cdp.frame: navigate inherits original fragment across redirect" { } } +test "cdp.frame: navigate to about:blank replaces a non-blank document" { + // Regression test for #2363. Page.navigate("about:blank") issued against a + // tab that already holds a real document must replace the active document + // with a fresh about:blank page — not leave the previous page in place. + // A synthetic (no-HTTP) navigation has no response-headers callback to + // commit a pending Page, so it must swap the active Page immediately. + var ctx = try testing.context(); + defer ctx.deinit(); + + var bc = try ctx.loadBrowserContext(.{ .id = "BID-AB", .url = "hi.html", .target_id = "TID-AB-0000000".* }); + + // Precondition: the tab is on a non-blank document. + { + const frame = bc.session.currentFrame() orelse unreachable; + try testing.expect(std.mem.endsWith(u8, frame.url, "/hi.html")); + } + + try ctx.processMessage(.{ .id = 70, .method = "Page.navigate", .params = .{ .url = "about:blank" } }); + { + var runner = try bc.session.runner(.{}); + try runner.wait(.{ .ms = 2000 }); + } + + // The active frame must now point at the replaced about:blank document. + const frame = bc.session.currentFrame() orelse unreachable; + try testing.expectEqualSlices(u8, "about:blank", frame.url); + + // ...and the active page's JS context must agree — the exact symptom in the + // bug report was window.location.href staying on the previous URL. + var ls: js.Local.Scope = undefined; + frame.js.localScope(&ls); + defer ls.deinit(); + const v = try ls.local.exec("window.location.href === 'about:blank'", null); + try testing.expect(v.toBool()); +} + test "cdp.frame: anchor click sends Referer matching the originating page" { // HTML Living Standard "navigate" algorithm + Fetch §4.5 "request's referrer": // when a navigation is initiated by a hyperlink click (or form submit, or