Skip to content

Commit 2df3cfb

Browse files
committed
handle no js cases and suspended thrown redirects
1 parent 89d22df commit 2df3cfb

File tree

4 files changed

+190
-14
lines changed

4 files changed

+190
-14
lines changed

integration/rsc/rsc-nojs-test.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import getPort from "get-port";
33

44
import { implementations, js, setupRscTest, validateRSCHtml } from "./utils";
55

6-
test.use({ javaScriptEnabled: false });
7-
86
implementations.forEach((implementation) => {
97
test.describe(`RSC nojs (${implementation.name})`, () => {
108
let port: number;
@@ -20,6 +18,34 @@ implementations.forEach((implementation) => {
2018
implementation,
2119
port,
2220
files: {
21+
"src/routes.ts": js`
22+
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
23+
24+
export const routes = [
25+
{
26+
id: "root",
27+
path: "",
28+
lazy: () => import("./routes/root"),
29+
children: [
30+
{
31+
id: "home",
32+
index: true,
33+
lazy: () => import("./routes/home"),
34+
},
35+
{
36+
id: "render-redirect-lazy",
37+
path: "/render-redirect/lazy/:id?",
38+
lazy: () => import("./routes/render-redirect/lazy"),
39+
},
40+
{
41+
id: "render-redirect",
42+
path: "/render-redirect/:id?",
43+
lazy: () => import("./routes/render-redirect/home"),
44+
},
45+
],
46+
},
47+
] satisfies RSCRouteConfig;
48+
`,
2349
"src/routes/home.actions.ts": js`
2450
"use server";
2551
import { redirect } from "react-router";
@@ -76,6 +102,50 @@ implementations.forEach((implementation) => {
76102
);
77103
}
78104
`,
105+
106+
"src/routes/render-redirect/home.tsx": js`
107+
import { Link, redirect } from "react-router";
108+
109+
export default function RenderRedirect({ params: { id } }) {
110+
if (id === "redirect") {
111+
throw redirect("/render-redirect/redirected");
112+
}
113+
114+
return (
115+
<>
116+
<h1>{id || "home"}</h1>
117+
<Link to="/render-redirect/redirect">Redirect</Link>
118+
</>
119+
)
120+
}
121+
`,
122+
"src/routes/render-redirect/lazy.tsx": js`
123+
import { Suspense } from "react";
124+
import { Link, redirect } from "react-router";
125+
126+
export default function RenderRedirect({ params: { id } }) {
127+
return (
128+
<Suspense fallback={<p>Loading...</p>}>
129+
<Lazy id={id} />
130+
</Suspense>
131+
);
132+
}
133+
134+
async function Lazy({ id }) {
135+
await new Promise((r) => setTimeout(r, 0));
136+
137+
if (id === "redirect") {
138+
throw redirect("/render-redirect/lazy/redirected");
139+
}
140+
141+
return (
142+
<>
143+
<h1>{id || "home"}</h1>
144+
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
145+
</>
146+
);
147+
}
148+
`,
79149
},
80150
});
81151
});
@@ -129,5 +199,27 @@ implementations.forEach((implementation) => {
129199
// Ensure this is using RSC
130200
validateRSCHtml(await page.content());
131201
});
202+
203+
test("Suppport throwing redirect Response from render", async ({
204+
page,
205+
}) => {
206+
await page.goto(`http://localhost:${port}/render-redirect`);
207+
await expect(page.getByText("home")).toBeAttached();
208+
await page.click("a");
209+
await page.waitForURL(
210+
`http://localhost:${port}/render-redirect/redirected`,
211+
);
212+
await expect(page.getByText("redirected")).toBeAttached();
213+
});
214+
215+
test("Suppport throwing redirect Response from suspended render", async ({
216+
page,
217+
}) => {
218+
await page.goto(`http://localhost:${port}/render-redirect/lazy/redirect`);
219+
await page.waitForURL(
220+
`http://localhost:${port}/render-redirect/lazy/redirected`,
221+
);
222+
await expect(page.getByText("redirected")).toBeAttached();
223+
});
132224
});
133225
});

integration/rsc/rsc-test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,11 @@ implementations.forEach((implementation) => {
535535
path: "action-transition-state",
536536
lazy: () => import("./routes/action-transition-state/home"),
537537
},
538+
{
539+
id: "render-redirect-lazy",
540+
path: "/render-redirect/lazy/:id?",
541+
lazy: () => import("./routes/render-redirect/lazy"),
542+
},
538543
{
539544
id: "render-redirect",
540545
path: "/render-redirect/:id?",
@@ -1482,6 +1487,33 @@ implementations.forEach((implementation) => {
14821487
)
14831488
}
14841489
`,
1490+
"src/routes/render-redirect/lazy.tsx": js`
1491+
import { Suspense } from "react";
1492+
import { Link, redirect } from "react-router";
1493+
1494+
export default function RenderRedirect({ params: { id } }) {
1495+
return (
1496+
<Suspense fallback={<p>Loading...</p>}>
1497+
<Lazy id={id} />
1498+
</Suspense>
1499+
);
1500+
}
1501+
1502+
async function Lazy({ id }) {
1503+
await new Promise((r) => setTimeout(r, 0));
1504+
1505+
if (id === "redirect") {
1506+
throw redirect("/render-redirect/lazy/redirected");
1507+
}
1508+
1509+
return (
1510+
<>
1511+
<h1>{id || "home"}</h1>
1512+
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
1513+
</>
1514+
);
1515+
}
1516+
`,
14851517
},
14861518
});
14871519
});
@@ -1765,7 +1797,23 @@ implementations.forEach((implementation) => {
17651797
page,
17661798
}) => {
17671799
await page.goto(`http://localhost:${port}/render-redirect`);
1800+
await expect(page.getByText("home")).toBeAttached();
17681801
await page.click("a");
1802+
await page.waitForURL(
1803+
`http://localhost:${port}/render-redirect/redirected`,
1804+
);
1805+
await expect(page.getByText("redirected")).toBeAttached();
1806+
});
1807+
1808+
test("Suppport throwing redirect Response from suspended render", async ({
1809+
page,
1810+
}) => {
1811+
await page.goto(`http://localhost:${port}/render-redirect/lazy`);
1812+
await expect(page.getByText("home")).toBeAttached();
1813+
await page.click("a");
1814+
await page.waitForURL(
1815+
`http://localhost:${port}/render-redirect/lazy/redirected`,
1816+
);
17691817
await expect(page.getByText("redirected")).toBeAttached();
17701818
});
17711819
});

packages/react-router/lib/hooks.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,7 +1088,7 @@ export class RenderErrorBoundary extends React.Component<
10881088
}
10891089
}
10901090

1091-
const errorRedirectPromises = new WeakMap<any, Promise<void>>();
1091+
const errorRedirectHandledMap = new WeakMap<any, boolean>();
10921092
function RSCErrorHandler({
10931093
children,
10941094
error,
@@ -1104,15 +1104,22 @@ function RSCErrorHandler({
11041104
) {
11051105
let redirect = decodeRedirectErrorDigest(error.digest);
11061106
if (redirect) {
1107-
let promise = errorRedirectPromises.get(error);
1108-
if (!promise) {
1107+
if (
1108+
typeof window !== "undefined" &&
1109+
window.__reactRouterDataRouter &&
1110+
!errorRedirectHandledMap.get(error)
1111+
) {
11091112
// TODO: Handle external redirects?
1110-
promise = window.__reactRouterDataRouter!.navigate(redirect.location, {
1111-
replace: true,
1112-
});
1113-
errorRedirectPromises.set(error, promise);
1113+
setTimeout(() => {
1114+
window.__reactRouterDataRouter!.navigate(redirect.location, {
1115+
replace: true,
1116+
});
1117+
}, 0);
1118+
errorRedirectHandledMap.set(error, true);
11141119
}
1115-
throw promise;
1120+
return (
1121+
<meta httpEquiv="refresh" content={`0;url=${redirect.location}`} />
1122+
);
11161123
}
11171124
}
11181125
return children;

packages/react-router/lib/rsc/server.ssr.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { RSCPayload } from "./server.rsc";
1111
import { createRSCRouteModules } from "./route-modules";
1212
import { isRouteErrorResponse } from "../router/utils";
1313
import { decodeRedirectErrorDigest } from "../errors";
14+
import { escapeHtml } from "../dom/ssr/markup";
1415

1516
type DecodedPayload = Promise<RSCPayload> & {
1617
_deepestRenderedBoundaryId?: string | null;
@@ -248,8 +249,20 @@ export async function routeRSCServerRequest({
248249
});
249250
}
250251

252+
const redirectTransform = new TransformStream({
253+
flush(controller) {
254+
if (renderRedirect) {
255+
controller.enqueue(
256+
new TextEncoder().encode(
257+
`<meta http-equiv="refresh" content="0;url=${escapeHtml(renderRedirect.location)}"/>`,
258+
),
259+
);
260+
}
261+
},
262+
});
263+
251264
if (!hydrate) {
252-
return new Response(html, {
265+
return new Response(html.pipeThrough(redirectTransform), {
253266
status: serverResponse.status,
254267
headers,
255268
});
@@ -259,7 +272,9 @@ export async function routeRSCServerRequest({
259272
throw new Error("Failed to clone server response");
260273
}
261274

262-
const body = html.pipeThrough(injectRSCPayload(serverResponseB.body));
275+
const body = html
276+
.pipeThrough(injectRSCPayload(serverResponseB.body))
277+
.pipeThrough(redirectTransform);
263278
return new Response(body, {
264279
status: serverResponse.status,
265280
headers,
@@ -354,8 +369,20 @@ export async function routeRSCServerRequest({
354369
});
355370
}
356371

372+
const retryRedirectTransform = new TransformStream({
373+
flush(controller) {
374+
if (retryRedirect) {
375+
controller.enqueue(
376+
new TextEncoder().encode(
377+
`<meta http-equiv="refresh" content="0;url=${escapeHtml(retryRedirect.location)}"/>`,
378+
),
379+
);
380+
}
381+
},
382+
});
383+
357384
if (!hydrate) {
358-
return new Response(html, {
385+
return new Response(html.pipeThrough(retryRedirectTransform), {
359386
status: status,
360387
headers,
361388
});
@@ -365,7 +392,9 @@ export async function routeRSCServerRequest({
365392
throw new Error("Failed to clone server response");
366393
}
367394

368-
const body = html.pipeThrough(injectRSCPayload(serverResponseB.body));
395+
const body = html
396+
.pipeThrough(injectRSCPayload(serverResponseB.body))
397+
.pipeThrough(retryRedirectTransform);
369398
return new Response(body, {
370399
status,
371400
headers,

0 commit comments

Comments
 (0)