You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Protected routes: write the guard as a server function
Document the intended behavior rather than the current router gap
(solidjs/solid-router#603): requireUser is a "use server" function
wrapped in query that throws redirect(), and the router navigates on it
during server rendering and client-side navigations alike. Remove the
caution that described the workaround.
Co-authored-by: Cursor <cursoragent@cursor.com>
if (!user) throwredirect(paths["sign-in"]({ next }));
127
128
returnuser;
@@ -133,22 +134,15 @@ Click **Account** in the header signed out and the URL becomes the same sign-in
133
134
Sign in, and the next visit to the account area runs the check again and finds the user, because a router action's completion invalidates every cached query.
134
135
135
136
The preload is what starts the check as soon as the route matches, and on hover, before `RequireUser` exists; the memo is what holds the navigation until the check answers.
136
-
Both call the same function, and the function has no `"use server"` directive of its own.
137
-
It runs where the read happens, on the server during server rendering and in the browser during a navigation, and calls `getCurrentUser` across the boundary.
138
-
139
-
:::caution[Throw the redirect in the query, not inside the server function]
140
-
Moving the `throw redirect(...)` into a `"use server"` body and wrapping that function in `query` redirects on a full page request, because the function runs in-process during server rendering, but not on a client-side navigation.
141
-
The server-function transport delivers a redirect to its caller as a response object with the target in an `X-Server-Function-Redirect` header and no `Location`; `query` navigates only on `Location`, so the read settles with that object and the navigation completes as if the check had passed.
142
-
Router actions decode that header, which is why `throw redirect()` inside a server function works from a form or an action.
143
-
For a read, build the redirect in the function `query` wraps, as above.
144
-
:::
137
+
Both call the same function.
138
+
Its body runs on the server either way: in-process during server rendering, where it shares the request's `getCurrentUser` result with the header, and through the server-function endpoint during a navigation, where the thrown redirect travels back to the router as the response.
145
139
146
140
Hovering the **Account** link runs the preload with the `"preload"` intent, which starts the request and does not act on its outcome, so a hover never redirects; the click that follows does, whether it reuses that answer or fetches a new one.
147
141
148
142
:::deep-dive[What the router does with a thrown redirect]
149
143
`query` treats a value thrown by the wrapped function the same way as a returned one.
150
-
When the value is a `Response`, the router copies its headers onto the request event's response and reads its `Location` header.
151
-
A relative `Location` becomes a call to the router's `navigate` with `replace: true`.
144
+
When the value is a redirect `Response`, whether thrown in-process or delivered by the server-function transport, the router copies its headers onto the request event's response and reads the target.
145
+
A same-origin target becomes a call to the router's `navigate` with `replace: true`.
152
146
On the server, `navigate` records a 302 with that `Location` on the request event, and the read resolves `undefined` so the render can finish; a `Location` set before the shell flushes turns the whole response into a redirect with no body, and one set after the flush is appended to the stream as a script that sets `window.location`, as the [streaming renderer](/concepts/rendering-and-ssr#streaming-rendering) documents.
153
147
In the browser the read stays pending forever, because the navigation unmounts everything that was waiting on it; the account page never receives a value, `undefined` included.
154
148
Any `X-Revalidate` keys on the redirect are invalidated before the navigation so the destination fetches fresh.
@@ -332,7 +326,7 @@ Chained client-side redirects stop after 100 with `Too many redirects` from the
332
326
### The account page paints, then the sign-in page replaces it
333
327
334
328
The guard navigates after the user data has arrived, from an effect or a callback, so the page commits and the navigation follows it.
335
-
Throw the redirect from inside the `query`function instead; the read stays pending, the navigation is held, and the sign-in page replaces the target before anything paints.
329
+
Throw the redirect from inside the server function the `query`wraps instead; the read stays pending, the navigation is held, and the sign-in page replaces the target before anything paints.
336
330
On a full page request, add the middleware so the server answers with a redirect status instead of rendering the shell first.
337
331
338
332
### The server function returns orders for a signed-out caller
@@ -349,7 +343,7 @@ Accept only paths that start with a single `/` and reject `//` and `/\`; run tha
349
343
350
344
- Decide once, on `locals.userId`, and run the decision in every server function, in the route guard, and in middleware; each layer covers a way in that the others do not see.
351
345
- Put the account pages under a pathless route whose `preload` starts the check and whose component renders `props.children` behind it, and keep `/sign-in` beside the group.
352
-
- Make the guard a `query` around a plain async function that calls a server function and throws `redirect()`itself, so the router sees the `Location`on the server and in the browser.
346
+
- Make the guard a server function wrapped in `query`that throws `redirect()`when the session has no user; the router navigates on it during server rendering and on client-side navigations alike.
353
347
- Build the sign-in target with `paths["sign-in"]({ next })`; the search object encodes the path.
354
348
- Validate `next` inside the server function: a single leading `/`, no `//`, no `/\`, and not the sign-in page itself.
355
349
- Return a redirect from middleware for full page requests under `/account`, and keep the function checks, because middleware does not see `/_server` or client-side navigations.
@@ -360,4 +354,4 @@ Accept only paths that start with a single `/` and reject `//` and `/\`; run tha
360
354
-[Sessions and auth](/building-apps/sessions-and-auth): the cookie, `getSession()`, and the middleware that puts `userId` on the event this guide reads.
361
355
-[Middleware and API routes](/building-apps/middleware-and-api-routes): the chain the `requireAccountSession` middleware joins, and how to protect API routes the same way.
362
356
-[Forms](/guides/forms): inline validation messages and pending state for the sign-in form.
363
-
-[SSR-safe code](/guides/ssr-safe-code): the checklist for code such as `requireUser` that runs on both sides.
357
+
-[SSR-safe code](/guides/ssr-safe-code): the checklist for code such as `RequireUser` that runs on both sides.
Copy file name to clipboardExpand all lines: src/routes/(7)glossary.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -388,7 +388,7 @@ See [Derive a store with a projection](/concepts/stores#derive-a-store-with-a-pr
388
388
389
389
### redirect
390
390
391
-
Returns a `Response` with a `Location` header from a server function: a router action navigates without a page load, a form post without JavaScript follows a 303, and plain code receives the `Response` object.
391
+
Returns a `Response` with a `Location` header from a server function: a router action or query navigates without a page load, a form post without JavaScript follows a 303, and plain code receives the `Response` object.
392
392
393
393
See [Redirect the caller](/building-apps/server-functions/mutations-and-responses#redirect-the-caller).
0 commit comments