Repository navigation
Preloaded redirect results are cached and replayed, causing stale redirects and redirect loops #16484
Description
Activity
Heads up that this is on
version-3too, and a bit worse there._preload_datastores every result inload_cache, andload_routehands it straight back when the ids match ("the preload becomes the real navigation") without re-runningload, with nothing nulling the cache afterwards.discard_load_cacheonly fires on invalidation,refreshAll, a different intent id, a rejected promise and a fork mismatch, never on the result type, so a preloaded redirect gets replayed exactly like it does onmain.The extra bit: v2's
// Don't cache errors, because they might be transientguard didn't survive the rewrite, so on v3 preloaded errors are cached as well.I have the main fix in #16486. Happy to open the version-3 counterpart if you want it.
v2's
// Don't cache errors, because they might be transientguard didn't survive the rewrite, so on v3 preloaded errors are cached as well.This part isn't right, preload errors reject on v3 and that discards the cache (client.js:767). The redirect half is right. The cache is never discarded based on result type, and the redirect recursion in navigate() returns before the cache reset is reached, so a preloaded redirect replays just like on main.
Correcting myself on the
version-3part:navigate()setsload_cache = nullwhen a navigation commits, so the plain replay I described doesn't apply there. A loop would need routes that keep redirecting into each other with nothing ever committing, and I haven't tested that, so treat my earlier comment as unverified for v3. The behaviour onmaindescribed in the issue stands.- addedbugSomething isn't workingSomething isn't workingand removedbugSomething isn't workingSomething isn't working
on Aug 4, 2026 - linked a pull request that will close this issuefix: re-evaluate preloaded redirects before navigation #16930
on Aug 25, 2026 The actual issue is that we're reusing the same load cache on the second
navigatecall, which is really suppose to grab a fresh load result, but doesn't because the load ID is never updatedkit/packages/kit/src/runtime/client/client.js
Line 1413 in 36c7256
if (!action_result && load_cache?.id === id) { We could delete the load cache before that happens, specifically at
but that just makes the preload redundantkit/packages/kit/src/runtime/client/client.js
Line 2072 in 36c7256
if (navigation_result.type === 'redirect') { I have a feeling that the better solution would be to see the redirect through (similar to what the native
fetchdoes by default) and cache the final result instead so that the preload actually makes the navigation instantaneous. @dummdidumm any idea why we never did that in the first place and have thistype: redirectload result?I honestly can't remember anymore. I think it's because we always thought of the
loadedstate as "this is for the page you requested" and not "the end result"Reacted by Tee Ming- added a commit that references this issue
on Aug 27, 2026
Describe the bug
When a link is data-preloaded (
data-sveltekit-preload-data, on hover or tap) and the target route'sloadreturns aredirect(...), 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_cacheis only cleared on a committed navigation. So the stale redirect is replayed every cycle until the 20-redirect limit throwsError: 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.jssrc/routes/dashboard/+page.server.jssrc/routes/select/+page.server.jssrc/routes/start/+page.svelteSteps:
/start./dashboard/__data.jsonwhileselectedisfalse, so/dashboard's load redirects to/select, and that redirect result is cached for/dashboard.Expected:
/dashboard→/select(setsselected = true) →/dashboardrenders. One bounce, resolves.Actual:
/dashboardreplays the cached redirect to/select;/selectredirects back to/dashboard; the cached/dashboardredirect is replayed again — never re-fetched, so it never observesselected === true— and it loops untilError: 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_datacaches theload_routeresult and only discards it when it is aloadedresult with an error (// Don't cache errors, because they might be transient). Aredirectresult is kept.load_routeshort-circuits on a cache hit:if (load_cache?.id === id) { … return load_cache.promise; }— returning the cached redirect with no fetch.navigate, theredirectbranch (redirect_count < 20) recurses and returns before theload_cache = nullon the commit path — so a redirect-only chain never clears the cache.Expected behaviour
A preloaded
redirectresult shouldn't be replayed as a navigation decision. The most consistent fix looks like discardingredirectresults in_preload_datathe 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 (
querycaches 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