Skip to content

Commit 4e0331b

Browse files
bartlomiejuclaude
andauthored
fix: preserve partial search param through redirects (#3715)
## Summary - Fixes #2610 — "Partial + redirect breaks islands" - When a partial request (`?fresh-partial=true`) hits a server-side redirect via `ctx.redirect()`, the `fresh-partial` search param is now preserved in the redirect `Location` header - Previously the param was lost, so the redirect target rendered as a full page instead of a partial response, breaking island hydration The fix is in `ctx.redirect()` in `packages/fresh/src/context.ts` — when `this.isPartial` is true, the partial param is appended to the redirect location (handling existing query params and hash fragments correctly). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0015710 commit 4e0331b

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

packages/fresh/src/context.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ export class Context<State> {
182182
location = `${pathname.replaceAll(/\/+/g, "/")}${search}`;
183183
}
184184

185+
// Preserve the partial search param through redirects so that the
186+
// redirected page is still rendered in partial mode.
187+
if (this.isPartial) {
188+
const hashIdx = location.indexOf("#");
189+
const base = hashIdx > -1 ? location.slice(0, hashIdx) : location;
190+
const hash = hashIdx > -1 ? location.slice(hashIdx) : "";
191+
const separator = base.includes("?") ? "&" : "?";
192+
location = `${base}${separator}${PARTIAL_SEARCH_PARAM}=true${hash}`;
193+
}
194+
185195
return new Response(null, {
186196
status,
187197
headers: {

packages/fresh/src/context_test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,44 @@ Deno.test("ctx.render - throw with invalid first arg", async () => {
7070
expect(res.status).toEqual(500);
7171
});
7272

73+
Deno.test("ctx.redirect - preserves partial search param through redirects", async () => {
74+
const app = new App()
75+
.get("/old", (ctx) => ctx.redirect("/new"));
76+
const server = new FakeServer(app.handler());
77+
78+
// Normal redirect should not have partial param
79+
let res = await server.get("/old");
80+
expect(res.status).toEqual(302);
81+
expect(res.headers.get("Location")).toEqual("/new");
82+
83+
// Partial redirect should preserve the param
84+
res = await server.get("/old?fresh-partial=true");
85+
expect(res.status).toEqual(302);
86+
expect(res.headers.get("Location")).toEqual("/new?fresh-partial=true");
87+
88+
// Partial redirect with existing query params
89+
const app2 = new App()
90+
.get("/old", (ctx) => ctx.redirect("/new?foo=bar"));
91+
const server2 = new FakeServer(app2.handler());
92+
93+
res = await server2.get("/old?fresh-partial=true");
94+
expect(res.status).toEqual(302);
95+
expect(res.headers.get("Location")).toEqual(
96+
"/new?foo=bar&fresh-partial=true",
97+
);
98+
99+
// Partial redirect with hash fragment — param must come before the hash
100+
const app3 = new App()
101+
.get("/old", (ctx) => ctx.redirect("/new#section"));
102+
const server3 = new FakeServer(app3.handler());
103+
104+
res = await server3.get("/old?fresh-partial=true");
105+
expect(res.status).toEqual(302);
106+
expect(res.headers.get("Location")).toEqual(
107+
"/new?fresh-partial=true#section",
108+
);
109+
});
110+
73111
Deno.test("ctx.isPartial - should indicate whether request is partial or not", async () => {
74112
const isPartials: boolean[] = [];
75113
const app = new App()

0 commit comments

Comments
 (0)