From 944d4d327bcce7c10f1c886492af0a24c9770376 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Thu, 23 Apr 2026 00:21:44 +0200 Subject: [PATCH 1/3] fix: keep ?fresh-partial an implementation detail --- packages/fresh/src/runtime/client/partials.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fresh/src/runtime/client/partials.ts b/packages/fresh/src/runtime/client/partials.ts index ab7cbb35bb4..b8cf487f3f2 100644 --- a/packages/fresh/src/runtime/client/partials.ts +++ b/packages/fresh/src/runtime/client/partials.ts @@ -361,6 +361,7 @@ async function fetchPartials( actualUrl = nextUrl; } } + actualUrl.searchParams.delete(PARTIAL_SEARCH_PARAM); try { await applyPartials(res); From 91369f472377de35c6b5161c76b903df48db6486 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sun, 26 Apr 2026 04:18:10 +0200 Subject: [PATCH 2/3] fix: after redirect, should replace history instead of pushing it --- packages/fresh/src/runtime/client/partials.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/fresh/src/runtime/client/partials.ts b/packages/fresh/src/runtime/client/partials.ts index b8cf487f3f2..008e23a9592 100644 --- a/packages/fresh/src/runtime/client/partials.ts +++ b/packages/fresh/src/runtime/client/partials.ts @@ -83,7 +83,7 @@ if (!history.state) { history.replaceState(state, document.title); } -function maybeUpdateHistory(nextUrl: URL) { +function maybePushHistory(nextUrl: URL) { // Only add history entry when URL is new. Still apply // the partials because sometimes users click a link to // "refresh" the current page. @@ -106,6 +106,12 @@ function maybeUpdateHistory(nextUrl: URL) { } } +function maybeReplaceHistory(nextUrl: URL) { + if (nextUrl.href !== globalThis.location.href) { + history.replaceState(history.state, "", nextUrl.href); + } +} + document.addEventListener("click", async (e) => { let el = e.target; if (el && (el instanceof HTMLElement || el instanceof SVGElement)) { @@ -154,7 +160,7 @@ document.addEventListener("click", async (e) => { const nextUrl = new URL(el.href); try { - maybeUpdateHistory(nextUrl); + maybePushHistory(nextUrl); const partialUrl = new URL( partial ? partial : nextUrl.href, @@ -376,7 +382,7 @@ async function fetchPartials( } if (shouldNavigate) { - maybeUpdateHistory(actualUrl); + maybeReplaceHistory(actualUrl); } } From ff3683eb9ed47ecfaaa346d64d06b44c38155e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Sun, 26 Apr 2026 10:14:42 +0200 Subject: [PATCH 3/3] test: add tests for redirect history behavior - Redirect via link click should not create a back-button trap (back should go to the page before the redirect, not the redirect source) - Redirect should not leak ?fresh-partial in the URL bar --- packages/fresh/tests/partials_test.tsx | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/packages/fresh/tests/partials_test.tsx b/packages/fresh/tests/partials_test.tsx index 1ef3a78f5a8..9b4a17d510b 100644 --- a/packages/fresh/tests/partials_test.tsx +++ b/packages/fresh/tests/partials_test.tsx @@ -3498,3 +3498,93 @@ Deno.test({ }); }, }); + +Deno.test({ + name: "partials - redirect does not create back-button trap", + fn: async () => { + const app = testApp() + .get("/target", (ctx) => { + return ctx.render( + + +

target page

+
+
, + ); + }) + .get("/redirect", (ctx) => ctx.redirect("/target")) + .get("/", (ctx) => { + return ctx.render( + +
+ go + +

home

+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".init").wait(); + + // Click link that redirects + await page.locator(".nav").click(); + await page.locator(".done").wait(); + + // Should show the redirect target URL, not the intermediate + await page.waitForFunction(() => { + return window.location.pathname === "/target"; + }); + + // Going back should return to home, not to /redirect + await page.evaluate(() => window.history.go(-1)); + await page.locator(".init").wait(); + await page.waitForFunction(() => { + return window.location.pathname === "/"; + }); + }); + }, +}); + +Deno.test({ + name: "partials - redirect does not leak ?fresh-partial in URL", + fn: async () => { + const app = testApp() + .get("/target", (ctx) => { + return ctx.render( + + +

target page

+
+
, + ); + }) + .get("/redirect", (ctx) => ctx.redirect("/target")) + .get("/", (ctx) => { + return ctx.render( + +
+ go + +

home

+
+
+
, + ); + }); + + await withBrowserApp(app, async (page, address) => { + await page.goto(address, { waitUntil: "load" }); + await page.locator(".init").wait(); + + await page.locator(".nav").click(); + await page.locator(".done").wait(); + + const url = await page.evaluate(() => window.location.href); + expect(url).not.toContain("fresh-partial"); + }); + }, +});