Skip to content

Fix login redirect loop: replace cached /me actor, don't just invalidate - #22

Merged
luma-founder merged 1 commit into
mainfrom
fix-auth-cache-after-login
May 8, 2026
Merged

luma-founder merged 1 commit into
mainfrom
fix-auth-cache-after-login

Conversation

@luma-founder

Copy link
Copy Markdown

Summary

Login succeeded server-side (204 + Set-Cookie) but the SPA bounced the user back to /login. Root cause is purely frontend cache management.

Trace:

  1. Page loads → useMe query runs → 401 → cached value is null.
  2. User submits login → server returns 204, sets the session cookie.
  3. useLogin.onSuccess ran invalidateQueries(["me"]) — but with no active observer (the login form doesn't use useMe), this only marks the query stale. No refetch.
  4. navigate({ to: "/" }) fires → / route's beforeLoad calls ensureQueryData(meQueryOptions).
  5. ensureQueryData returns the cached null regardless of stale flag.
  6. Guard sees no actor → redirects back to /login.

fetchQuery would hit the same trap because meQueryOptions has staleTime: Infinity — fetchQuery returns cached when not stale.

Fix:

  • useLogin.onSuccess — removeQueries(["me"]) to clear the cache, then await fetchQuery(meQueryOptions) to plant the fresh actor. Next ensureQueryData call sees the new value synchronously.
  • useLogout.onSettled — setQueryData(["me"], null) directly. We already know the answer.

Test plan

  • 2 new regression tests (23 total): seed cache pre-state, run the mutation, assert the cache holds the expected post-state.
  • Live Docker against the API directly: pre-login /me → 401, login → 204 + cookie, post-login /me with same jar → 200 with actor. The backend was correct; only the frontend cache was the issue.

Login succeeded server-side (204 + Set-Cookie) but the SPA bounced the
user back to /login. Root cause: route `beforeLoad` guards use
`ensureQueryData(meQueryOptions)`, which returns the cached value
unconditionally. After the page-load /me probe failed (401, cached
null), `useLogin.onSuccess` ran `invalidateQueries(["me"])` — which only
marks the query stale and triggers a refetch *if there's an active
observer*. The login form has none. So the cached null stuck around;
beforeLoad saw it and redirected back to /login.

`fetchQuery` would have hit the same trap, because `meQueryOptions` has
`staleTime: Infinity` — fetchQuery returns cached when not stale.

Fix:

- `useLogin.onSuccess`: `removeQueries(["me"])` first to clear the
  cache, then `await fetchQuery(meQueryOptions)` to plant the fresh
  actor. The next ensureQueryData call (in beforeLoad) sees the new
  value synchronously.
- `useLogout.onSettled`: `setQueryData(["me"], null)` directly. We
  already know the answer; no need for a round trip.

Tests (2 new, 23 total):

- `useLogin > plants the fresh /me actor in the cache so route guards
  see it` — explicitly seeds null, runs the mutation, asserts the cache
  holds the new actor on settle. Locks in the regression.
- `useLogout > clears the cached actor on success so the login guard
  takes over`.

Verified against a live Docker container: pre-login /me → 401, login →
204 + cookie, post-login /me with same jar → 200 with actor. The
frontend was the only thing broken.
@luma-founder
luma-founder merged commit b71ca5b into main May 8, 2026
4 checks passed
@luma-founder
luma-founder deleted the fix-auth-cache-after-login branch May 8, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants