solidjs / solidjs/solid-router

query() does not decode X-Server-Function-Redirect, so a redirect() thrown inside a "use server" read only redirects during SSR

Open
#603 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
1.3k
Forks
180
Avg merge
1d 13h
Merged PRs (30d)
19

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/data/query.ts, especially handleResponse, and compare its redirect handling with src/data/action.ts and decodeRedirectHeaderValue. Run test/query-redirect.spec.tsx, then reproduce the client-side navigation case from the issue. Done means query handles X-Server-Function-Redirect like action, including same-origin navigation and a pending client read.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
full-stack
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.