Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit850bf9a

Browse files
authored
fix: handle rsc external redirects (#14400)
1 parent40e3966 commit850bf9a

File tree

3 files changed

+177
-2
lines changed

3 files changed

+177
-2
lines changed

‎.changeset/breezy-planes-roll.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router":patch
3+
---
4+
5+
handle external redirects in from server actions

‎integration/rsc/rsc-test.ts‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,17 @@ implementations.forEach((implementation) => {
435435
}
436436
]
437437
},
438+
{
439+
id: "throw-external-redirect-server-action",
440+
path: "throw-external-redirect-server-action",
441+
children: [
442+
{
443+
id: "throw-external-redirect-server-action.home",
444+
index: true,
445+
lazy: () => import("./routes/throw-external-redirect-server-action/home"),
446+
}
447+
]
448+
},
438449
{
439450
id: "side-effect-redirect-server-action",
440451
path: "side-effect-redirect-server-action",
@@ -446,6 +457,17 @@ implementations.forEach((implementation) => {
446457
}
447458
]
448459
},
460+
{
461+
id: "side-effect-external-redirect-server-action",
462+
path: "side-effect-external-redirect-server-action",
463+
children: [
464+
{
465+
id: "side-effect-external-redirect-server-action.home",
466+
index: true,
467+
lazy: () => import("./routes/side-effect-external-redirect-server-action/home"),
468+
}
469+
]
470+
},
449471
{
450472
id: "server-function-reference",
451473
path: "server-function-reference",
@@ -986,6 +1008,82 @@ implementations.forEach((implementation) => {
9861008
);
9871009
}
9881010
`,
1011+
"src/routes/throw-external-redirect-server-action/home.actions.ts":js`
1012+
"use server";
1013+
import { redirect } from "react-router";
1014+
1015+
export async function redirectAction(formData: FormData) {
1016+
// Throw a redirect to an external URL
1017+
throw redirect("https://example.com/");
1018+
}
1019+
`,
1020+
"src/routes/throw-external-redirect-server-action/home.client.tsx":js`
1021+
"use client";
1022+
1023+
import { useState } from "react";
1024+
1025+
export function Counter() {
1026+
const [count, setCount] = useState(0);
1027+
return <button type="button" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;
1028+
}
1029+
`,
1030+
"src/routes/throw-external-redirect-server-action/home.tsx":js`
1031+
import { redirectAction } from "./home.actions";
1032+
import { Counter } from "./home.client";
1033+
1034+
export default function HomeRoute(props) {
1035+
return (
1036+
<div>
1037+
<form action={redirectAction}>
1038+
<button type="submit" data-submit>
1039+
Redirect via Server Function
1040+
</button>
1041+
</form>
1042+
<Counter />
1043+
</div>
1044+
);
1045+
}
1046+
`,
1047+
"src/routes/side-effect-external-redirect-server-action/home.actions.ts":js`
1048+
"use server";
1049+
import { redirect } from "react-router";
1050+
1051+
export async function redirectAction() {
1052+
// Perform a side-effect redirect to an external URL
1053+
redirect("https://example.com/", { headers: { "x-test": "test" } });
1054+
return "redirected";
1055+
}
1056+
`,
1057+
"src/routes/side-effect-external-redirect-server-action/home.client.tsx":js`
1058+
"use client";
1059+
import { useState } from "react";
1060+
1061+
export function Counter() {
1062+
const [count, setCount] = useState(0);
1063+
return <button type="button" onClick={() => setCount(c => c + 1)} data-count>Count: {count}</button>;
1064+
}
1065+
`,
1066+
"src/routes/side-effect-external-redirect-server-action/home.tsx":js`
1067+
"use client";
1068+
import {useActionState} from "react";
1069+
import { redirectAction } from "./home.actions";
1070+
import { Counter } from "./home.client";
1071+
1072+
export default function HomeRoute(props) {
1073+
const [state, action] = useActionState(redirectAction, null);
1074+
return (
1075+
<div>
1076+
<form action={action}>
1077+
<button type="submit" data-submit>
1078+
Redirect via Server Function
1079+
</button>
1080+
</form>
1081+
{state && <div data-testid="state">{state}</div>}
1082+
<Counter />
1083+
</div>
1084+
);
1085+
}
1086+
`,
9891087

9901088
"src/routes/server-function-reference/home.actions.ts":js`
9911089
"use server";
@@ -1736,6 +1834,33 @@ implementations.forEach((implementation) => {
17361834
validateRSCHtml(awaitpage.content());
17371835
});
17381836

1837+
test("Supports React Server Functions thrown external redirects",async({
1838+
page,
1839+
})=>{
1840+
// Test is expected to fail currently — skip running it
1841+
// test.skip(true, "Known failing test for external redirect behavior");
1842+
1843+
awaitpage.goto(
1844+
`http://localhost:${port}/throw-external-redirect-server-action/`,
1845+
);
1846+
1847+
// Verify initial server render
1848+
awaitpage.waitForSelector("[data-count]");
1849+
expect(awaitpage.locator("[data-count]").textContent()).toBe(
1850+
"Count: 0",
1851+
);
1852+
awaitpage.click("[data-count]");
1853+
expect(awaitpage.locator("[data-count]").textContent()).toBe(
1854+
"Count: 1",
1855+
);
1856+
1857+
// Submit the form to trigger server function redirect to external URL
1858+
awaitpage.click("[data-submit]");
1859+
1860+
// We expect the browser to navigate to the external site (example.com)
1861+
awaitexpect(page).toHaveURL(`https://example.com/`);
1862+
});
1863+
17391864
test("Supports React Server Functions side-effect redirects",async({
17401865
page,
17411866
})=>{
@@ -1789,6 +1914,46 @@ implementations.forEach((implementation) => {
17891914
validateRSCHtml(awaitpage.content());
17901915
});
17911916

1917+
test("Supports React Server Functions side-effect external redirects",async({
1918+
page,
1919+
})=>{
1920+
// Test is expected to fail currently — skip running it
1921+
test.skip(implementation.name==="parcel","Not working in parcel?");
1922+
1923+
awaitpage.goto(
1924+
`http://localhost:${port}/side-effect-external-redirect-server-action`,
1925+
);
1926+
1927+
// Verify initial server render
1928+
awaitpage.waitForSelector("[data-count]");
1929+
expect(awaitpage.locator("[data-count]").textContent()).toBe(
1930+
"Count: 0",
1931+
);
1932+
awaitpage.click("[data-count]");
1933+
expect(awaitpage.locator("[data-count]").textContent()).toBe(
1934+
"Count: 1",
1935+
);
1936+
1937+
constresponseHeadersPromise=newPromise<Record<string,string>>(
1938+
(resolve)=>{
1939+
page.addListener("response",(response)=>{
1940+
if(response.request().method()==="POST"){
1941+
resolve(response.headers());
1942+
}
1943+
});
1944+
},
1945+
);
1946+
1947+
// Submit the form to trigger server function redirect to external URL
1948+
awaitpage.click("[data-submit]");
1949+
1950+
// We expect the browser to navigate to the external site (example.com)
1951+
awaitexpect(page).toHaveURL(`https://example.com/`);
1952+
1953+
// Optionally assert that the server sent the header
1954+
expect((awaitresponseHeadersPromise)["x-test"]).toBe("test");
1955+
});
1956+
17921957
test("Supports React Server Function References",async({ page})=>{
17931958
awaitpage.goto(`http://localhost:${port}/server-function-reference`);
17941959

‎packages/react-router/lib/rsc/browser.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function createCallServer({
140140
Promise.resolve(payloadPromise)
141141
.then(async(payload)=>{
142142
if(payload.type==="redirect"){
143-
if(payload.reload){
143+
if(payload.reload||isExternalLocation(payload.location)){
144144
window.location.href=payload.location;
145145
return()=>{};
146146
}
@@ -163,7 +163,7 @@ export function createCallServer({
163163
globalVar.__routerActionID<=actionId
164164
){
165165
if(rerender.type==="redirect"){
166-
if(rerender.reload){
166+
if(rerender.reload||isExternalLocation(rerender.location)){
167167
window.location.href=rerender.location;
168168
return;
169169
}
@@ -1047,3 +1047,8 @@ function debounce(callback: (...args: unknown[]) => unknown, wait: number) {
10471047
timeoutId=window.setTimeout(()=>callback(...args),wait);
10481048
};
10491049
}
1050+
1051+
functionisExternalLocation(location:string){
1052+
constnewLocation=newURL(location,window.location.href);
1053+
returnnewLocation.origin!==window.location.origin;
1054+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp