Skip to content

Preloaded redirect results are cached and replayed, causing stale redirects and redirect loops #16484

Description

@Hoffs

Describe the bug

When a link is data-preloaded (data-sveltekit-preload-data, on hover or tap) and the target route's load returns a redirect(...), the client stores that redirect result in its preload cache (load_cache). On the subsequent client-side navigation the cached redirect is replayed without re-running the load, so the redirect decision is never re-evaluated against current server state.

If the redirect target itself redirects back — two routes gating on opposite conditions — the client's redirect-follow recursion never commits a page, and load_cache is only cleared on a committed navigation. So the stale redirect is replayed every cycle until the 20-redirect limit throws Error: Redirect loop. This loops even when a fresh evaluation would resolve (i.e. after the gating condition has changed), because the cached leg never re-fetches.

Reproduction

Two routes gating on opposite states of a server-side flag, plus a preloaded link into one:

src/lib/state.js

export const state = { selected: false };

src/routes/dashboard/+page.server.js

import { redirect } from '@sveltejs/kit';
import { state } from '$lib/state.js';
export function load() {
  if (!state.selected) redirect(303, '/select');
  return {};
}

src/routes/select/+page.server.js

import { redirect } from '@sveltejs/kit';
import { state } from '$lib/state.js';
export function load() {
  state.selected = true;      // "select", then go to the gated page
  redirect(303, '/dashboard');
}

src/routes/start/+page.svelte

<a href="/dashboard" data-sveltekit-preload-data="hover">go to dashboard</a>

Steps:

  1. Visit /start.
  2. Hover or tap the link. This preloads /dashboard/__data.json while selected is false, so /dashboard's load redirects to /select, and that redirect result is cached for /dashboard.
  3. Click the link.

Expected: /dashboard/select (sets selected = true) → /dashboard renders. One bounce, resolves.

Actual: /dashboard replays the cached redirect to /select; /select redirects back to /dashboard; the cached /dashboard redirect is replayed again — never re-fetched, so it never observes selected === true — and it loops until Error: Redirect loop.

Without the preload (data-sveltekit-preload-data="off", or a full-page navigation) it resolves in one bounce — confirming the cached redirect is the cause, not the mutual redirect itself. A hard refresh also "fixes" it, because a fresh document load starts with an empty client cache.

Root cause (packages/kit/src/runtime/client/client.js, verified on 2.70.0)

  • _preload_data caches the load_route result and only discards it when it is a loaded result with an error (// Don't cache errors, because they might be transient). A redirect result is kept.
  • load_route short-circuits on a cache hit: if (load_cache?.id === id) { … return load_cache.promise; } — returning the cached redirect with no fetch.
  • In navigate, the redirect branch (redirect_count < 20) recurses and returns before the load_cache = null on the commit path — so a redirect-only chain never clears the cache.

Expected behaviour

A preloaded redirect result shouldn't be replayed as a navigation decision. The most consistent fix looks like discarding redirect results in _preload_data the same way errors are already discarded (a redirect is equally transient / server-state-dependent, and isn't renderable data), or otherwise re-evaluating rather than replaying it on consumption.

Severity / scope

Any app with two routes gating on opposite conditions (e.g. an onboarding/auth gate: a page requires X, the gate requires not-X) plus a preloaded link into one of them can hit an unrecoverable redirect loop until a hard refresh. Reproduces through @sveltejs/kit@2.70.0.

Related: #16192 (query caches a redirect response) — same theme in a different subsystem.

System Info

  • @sveltejs/kit: reproduced on 2.61.1 and verified still present through 2.70.0

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