Skip to content

navigate() from flight-data consumer can silently drop a relative-Location redirect (reliable with absolute URLs) #3107

Description

@sparklog

Summary

A redirect() thrown (or returned) from a server-function action with a relative target (Location: /) sometimes fails to navigate at all on the client — no error, no console output, no URL change — while the action response itself is perfectly valid and the single-flight query cache still updates. Sending the identical redirect as a same-origin absolute URL (Location: http://localhost:3000/) navigates reliably 100% of the time.

Versions:

  • @solidjs/router@2.0.0-next.18
  • @solidjs/web@2.0.0-rc.3
  • Solid 2.0.0-rc.3 (streaming SSR)

Reproduction

A login/register/logout implemented as a server-function action:

export const login = action(async (formData: FormData) => {
  'use server';
  // …validate, hash, setSession({ userId })…
  throw redirect('/'); // relative → SOMETIMES does not navigate
});

Observed wire response (identical whether the redirect is thrown or returned):

HTTP/1.1 200 OK
location: /
x-server-function-error: true
x-server-function-format: 0
x-single-flight: true
set-cookie: session=…; HttpOnly; Max-Age=604800; Path=/; SameSite=Lax; Secure

Expected: the client navigates to / after the action settles.

Actual: in some environments/browsers the page never navigates. A manual refresh reveals the correct state (the cookie was set server-side, and the single-flight data did update the query cache) — only the URL never moves. There are no console errors. In our reproduction environment (real headless Chrome, both dev and production builds) navigation succeeded, while a user's browser hit the failure consistently, so it is timing/environment-sensitive.

The same action with an absolute target always works:

throw redirect(new URL(path, getRequestEvent()!.request.url).toString()); // works reliably

Root-cause analysis

The client-side redirect policy is identical between the old @solidjs/router@0.14/0.15 (SolidStart 1.x era) and v2 — both do:

if (locationUrl.startsWith("http")) {
  window.location.href = locationUrl;   // hard navigation — reliable
} else {
  navigate(locationUrl);                 // soft navigation
}

The difference is the soft-navigation engine. The old router committed inside startTransition(() => setReference(...)) with a guaranteed .finally() teardown. The v2 router commits via setNavigateTarget(...) plus a cancelable queueMicrotask (@solidjs/router/dist/index.js, navigateFromRoute):

intent = "navigate";
lastTransitionTarget = newTarget;
if (firstNavigation) { setIsRouting(true); flush(); }
if (lastTransitionTarget === newTarget) {
  setNavigateTarget({ ...lastTransitionTarget });
  queueMicrotask(() => {
    if (lastTransitionTarget !== newTarget) return; // ← silently skips the URL commit
    intent = undefined;
    navigateEnd(lastTransitionTarget);
    setNavigateTarget(undefined);
    setIsRouting(false);
    lastTransitionTarget = undefined;
  });
}

If lastTransitionTarget is overwritten by a second navigation between setNavigateTarget and the microtask — or if a Solid 2 transition/flush scheduling interaction interferes — the URL commit is silently dropped. Additionally, for action redirects the navigate() call now originates from inside the flight-data consumer (setupFlightDataConsumerapplyResponseMetadata), i.e. before the action settles, which puts the navigation in a different async/timing window than the old router (which navigated from the action's settled handler).

Questions

  1. Is the lastTransitionTarget === newTarget guard in the microtask intended to silently drop superseded navigations? If so, is silently dropping a server-initiated redirect acceptable without any fallback (e.g. a hard location.assign to the last target)?
  2. Should redirects with a relative Location that originate from server functions prefer the hard branch (window.location.assign(new URL(locationUrl, location.href))), reserving soft navigation for same-document UI navigations?
  3. Is there a known interaction between Solid 2's "every write is a transition" model and calling navigate() from inside the flight-data consumer (pre-settle) that could make the microtask commit unreliable under certain hydration/timing conditions?

Expected behavior

A relative redirect from a server-function action should either navigate (soft or hard) or fail loudly — never silently do nothing while the cookie/data update succeeded.


  • Environment: Solid 2.0 rc, streaming SSR, @solidjs/server-functions transport with single-flight enabled.
  • Workaround currently shipped in our codebase: emit the redirect target as a same-origin absolute URL; verify before merging any opinionated fix.
  • Happy to provide a minimal repro repo or to bisect with a specific browser/timing setup if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions