Branch: next (f7602bf), against solid next (344ed054).
Summary
A redirect() thrown or returned inside a "use server" function that is wrapped in query() redirects on a full page request but not on a client-side navigation. The read settles with the Response object as its value and the navigation completes as if the check had passed.
Why
- The server-function transport masks redirects for scripted callers:
maskRedirect copies the target into X-Server-Function-Redirect (<status> <absolute-url>) and deletes Location (solid/packages/web/server-functions/src/server.ts ~2028–2034).
- The client transport returns such a response whole, untouched, for the integration to decode (
solid/packages/web/server-functions/src/client.ts ~744–757).
query.ts's handleResponse only checks v.headers.get("Location") before navigating (src/data/query.ts ~268–300). With Location gone, it falls through and the Response becomes the query's value.
action.ts does decode the carrier (decodeRedirectHeaderValue(metadata.headers.get(REDIRECT_HEADER)), src/data/action.ts ~473), which is why the same throw redirect() works from a router action.
test/query-redirect.spec.tsx covers a thrown Response that still carries Location, which is the SSR / in-process shape, so the gap is not exercised.
Repro
// src/data/account.ts
export const requireUser = query(async () => {
"use server";
const userId = getRequestEvent()?.locals.userId;
if (!userId) throw redirect("/sign-in");
return database.customers.find(userId);
}, "require-user");
Read requireUser() from a route preload or a memo under /account.
- Load
/account signed out with a full request: 302 to /sign-in. ✅
- Navigate to
/account signed out from another route (client-side): no navigation; the memo's value is a Response. ❌
Expected
query should treat a response carrying X-Server-Function-Redirect the way action does: decode it with decodeRedirectHeaderValue, navigate softly for same-origin targets, and hold the read pending on the client.
Workaround (what the docs currently describe)
Throw the redirect in the function query wraps, with the server function inside it:
export const requireUser = query(async () => {
const user = await getCurrentUser(); // "use server"
if (!user) throw redirect("/sign-in");
return user;
}, "require-user");
The docs' Protected routes guide documents this shape with a caution explaining the limitation; once query decodes the carrier, that caution can be removed.
Branch:
next(f7602bf), against solidnext(344ed054).Summary
A
redirect()thrown or returned inside a"use server"function that is wrapped inquery()redirects on a full page request but not on a client-side navigation. The read settles with theResponseobject as its value and the navigation completes as if the check had passed.Why
maskRedirectcopies the target intoX-Server-Function-Redirect(<status> <absolute-url>) and deletesLocation(solid/packages/web/server-functions/src/server.ts~2028–2034).solid/packages/web/server-functions/src/client.ts~744–757).query.ts'shandleResponseonly checksv.headers.get("Location")before navigating (src/data/query.ts~268–300). WithLocationgone, it falls through and the Response becomes the query's value.action.tsdoes decode the carrier (decodeRedirectHeaderValue(metadata.headers.get(REDIRECT_HEADER)),src/data/action.ts~473), which is why the samethrow redirect()works from a router action.test/query-redirect.spec.tsxcovers a thrown Response that still carriesLocation, which is the SSR / in-process shape, so the gap is not exercised.Repro
Read
requireUser()from a routepreloador a memo under/account./accountsigned out with a full request: 302 to/sign-in. ✅/accountsigned out from another route (client-side): no navigation; the memo's value is aResponse. ❌Expected
queryshould treat a response carryingX-Server-Function-Redirectthe wayactiondoes: decode it withdecodeRedirectHeaderValue, navigate softly for same-origin targets, and hold the read pending on the client.Workaround (what the docs currently describe)
Throw the redirect in the function
querywraps, with the server function inside it:The docs' Protected routes guide documents this shape with a caution explaining the limitation; once
querydecodes the carrier, that caution can be removed.