Redirects
A server function redirects by throwing redirect(to, { status }).
A server function redirects by throwing redirect(to, { status }). The wire carries it
as data, not as an HTTP redirect: the answer is 200 with { redirect: { to, status } }, because a fetch that
followed a 3xx would hand the stub an unrelated page.
- On the server, the target is checked first: a path on this site (
/notes/1, not//host), or an absolute URL of this origin or one listed intrustedOrigins. Anything else is a bug or an open-redirect attempt; the handler reportsREDIRECT_TARGET_BLOCKEDtoonErrorand answers 500. - In the browser, the stub resolves
toagainst the current page and requires the same origin, or it throws aServerFnProtocolError. It then throws the router'sredirect()signal. A loader orbeforeLoadthat awaits the call lets the router follow it. A call that nobody catches, such as one in an event handler, rejects with the signal, and theunhandledrejectionlistener thathydrateDuskinstalls navigates to it.
src/server/forms.tsts
import { redirect, serverFn } from "@shadow-js/dusk/server";
import { newNote, noteId } from "../schemas";
import type { Note } from "../types";
import { requireUser, userKey } from "./auth";
import { notes } from "./db.server";
export const openNote = serverFn({ method: "GET", input: noteId }, async ({ id }): Promise<never> => {
throw redirect(`/notes/${id}`, { status: 303 });
});
export const elsewhere = serverFn({}, async (): Promise<never> => {
throw redirect("https://evil.example/phish");
});
const pinned = new Set<string>();
export const pinNote = serverFn({ form: true, input: noteId }, async ({ id }): Promise<void> => {
pinned.add(id);
});
export const submitNote = serverFn(
{ form: true, input: newNote, middleware: [requireUser] },
async ({ title }, event): Promise<never> => {
const note: Note = { id: String(notes.size + 1), title, owner: event.get(userKey).id };
notes.set(note.id, note);
throw redirect(`/notes/${note.id}`);
},
);