Skip to content
17 changes: 17 additions & 0 deletions e2e/fixtures/ssr-redirect/src/pages/action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { unstable_redirect as redirect } from 'waku/router/server';

export default async function ActionPage() {
return (
<div>
<h1>Action Page</h1>
<form
action={async () => {
'use server';
redirect('/destination', 303);
}}
>
<button type="submit">Redirect Action</button>
</form>
</div>
);
}
18 changes: 18 additions & 0 deletions e2e/ssr-redirect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,22 @@ test.describe(`ssr-redirect`, () => {
await page.click("a[href='/async']");
await expect(page.getByRole('heading')).toHaveText('Destination Page');
});

test('navigation in server action', async ({ page }) => {
await page.goto(`http://localhost:${port}/action`);
await expect(page.getByRole('heading')).toHaveText('Action Page');
await page.click('text=Redirect Action');
await expect(page.getByRole('heading')).toHaveText('Destination Page');
});

test('navigation in server action (no js)', async ({ browser }) => {
const context = await browser.newContext({
javaScriptEnabled: false,
});
const page = await context.newPage();
await page.goto(`http://localhost:${port}/action`);
await expect(page.getByRole('heading')).toHaveText('Action Page');
await page.click('text=Redirect Action');
await expect(page.getByRole('heading')).toHaveText('Destination Page');
});
});
11 changes: 7 additions & 4 deletions packages/waku/src/router/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,24 +462,25 @@ const Redirect = ({
error,
to,
reset,
handledErrorSet,
}: {
error: unknown;
to: string;
reset: () => void;
handledErrorSet: WeakSet<object>;
}) => {
const router = useContext(RouterContext);
if (!router) {
throw new Error('Missing Router');
}
const { changeRoute } = router;
const handledErrorSet = useRef(new WeakSet());
useEffect(() => {
// ensure single re-fetch per server redirection error on StrictMode
// https://github.com/wakujs/waku/pull/1512
if (handledErrorSet.current.has(error as object)) {
if (handledErrorSet.has(error as object)) {
return;
}
handledErrorSet.current.add(error as object);
handledErrorSet.add(error as object);

const url = new URL(to, window.location.href);
// FIXME this condition seems too naive
Expand Down Expand Up @@ -511,14 +512,15 @@ const Redirect = ({
);
}
});
}, [error, to, reset, changeRoute]);
}, [error, to, reset, changeRoute, handledErrorSet]);
return null;
};

class CustomErrorHandler extends Component<
{ has404: boolean; children?: ReactNode },
{ error: unknown | null }
> {
private handledErrorSet = new WeakSet();
constructor(props: { has404: boolean; children?: ReactNode }) {
super(props);
this.state = { error: null };
Expand All @@ -545,6 +547,7 @@ class CustomErrorHandler extends Component<
error,
to: info.location,
reset: this.reset,
handledErrorSet: this.handledErrorSet,
});
}
throw error;
Expand Down
26 changes: 22 additions & 4 deletions packages/waku/src/router/define-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function unstable_notFound(): never {

export function unstable_redirect(
location: string,
status: 307 | 308 = 307,
status: 303 | 307 | 308 = 307,
): never {
throw createCustomError('Redirect', { status, location });
}
Expand Down Expand Up @@ -433,9 +433,27 @@ export function unstable_defineRouter(fns: {
});
};
setRerender(rerender);
const value = await input.fn(...input.args);
rendered = true;
return renderRsc({ ...(await elementsPromise), _value: value });
try {
const value = await input.fn(...input.args);
return renderRsc({ ...(await elementsPromise), _value: value });
} catch (e) {
const info = getErrorInfo(e);
if (info?.location) {
const rscPath = encodeRoutePath(info.location);
const entries = await getEntries(
rscPath,
undefined,
input.req.headers,
);
if (!entries) {
unstable_notFound();
}
return renderRsc(entries);
}
throw e;
} finally {
rendered = true;
}
}
const pathConfigItem = await getPathConfigItem(input.pathname);
if (pathConfigItem?.specs?.isApi && fns.handleApi) {
Expand Down
Loading