From df6f5f367b3184b900bae88d738c97bbe747eb48 Mon Sep 17 00:00:00 2001 From: Ondrej Ulehla Date: Thu, 23 Jul 2026 17:36:09 +0200 Subject: [PATCH] fix: don't cache preloaded redirect results The preload cache stored redirect results from load and replayed them on navigation without re-running load. Since the cache is only cleared once a navigation commits and a redirect never commits one, a preloaded redirect into a mutually-redirecting pair looped until the 20-redirect limit, even when a fresh evaluation would resolve. Discard the cache when the preload resolves to a redirect, matching the existing behavior for errors: both are transient results that must be re-evaluated at navigation time. Fixes #16484 --- .changeset/preload-redirect-cache.md | 5 +++++ packages/kit/src/runtime/client/client.js | 7 +++++-- .../redirect-gate/dashboard/+page.server.js | 9 +++++++++ .../redirect-gate/dashboard/+page.svelte | 1 + .../redirect-gate/select/+page.server.js | 7 +++++++ .../redirect-gate/start/+page.server.js | 6 ++++++ .../redirect-gate/start/+page.svelte | 5 +++++ .../preload-data/redirect-gate/state.js | 1 + .../kit/test/apps/basics/test/client.test.js | 18 ++++++++++++++++++ 9 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changeset/preload-redirect-cache.md create mode 100644 packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/select/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.server.js create mode 100644 packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/state.js diff --git a/.changeset/preload-redirect-cache.md b/.changeset/preload-redirect-cache.md new file mode 100644 index 000000000000..fe08ba924b67 --- /dev/null +++ b/.changeset/preload-redirect-cache.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: don't cache preloaded redirect results, so navigation re-runs `load` instead of replaying a stale redirect diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index a0b858ba0ddb..20685fe0c3a7 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -622,8 +622,11 @@ async function _preload_data(intent) { token: preload, promise: load_route({ ...intent, preload }).then((result) => { preload_tokens.delete(preload); - if (result.type === 'loaded' && result.state.error) { - // Don't cache errors, because they might be transient + if (result.type === 'redirect' || (result.type === 'loaded' && result.state.error)) { + // Don't cache errors or redirects, because they might be transient. + // A cached redirect would be replayed without re-running `load` + // until a navigation commits, which can turn a resolvable + // redirect chain into a redirect loop discard_load_cache(); } return result; diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.server.js b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.server.js new file mode 100644 index 000000000000..20425a680b33 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.server.js @@ -0,0 +1,9 @@ +import { redirect } from '@sveltejs/kit'; +import { state } from '../state.js'; + +export function load() { + if (!state.selected) { + redirect(303, '/data-sveltekit/preload-data/redirect-gate/select'); + } + return {}; +} diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.svelte new file mode 100644 index 000000000000..8b53980d76d2 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/dashboard/+page.svelte @@ -0,0 +1 @@ +

dashboard

diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/select/+page.server.js b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/select/+page.server.js new file mode 100644 index 000000000000..e310689bf9d2 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/select/+page.server.js @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import { state } from '../state.js'; + +export function load() { + state.selected = true; + redirect(303, '/data-sveltekit/preload-data/redirect-gate/dashboard'); +} diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.server.js b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.server.js new file mode 100644 index 000000000000..8c5dd3542d24 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.server.js @@ -0,0 +1,6 @@ +import { state } from '../state.js'; + +export function load() { + // reset so the test can run repeatedly against the same server + state.selected = false; +} diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.svelte b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.svelte new file mode 100644 index 000000000000..889614be7758 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/start/+page.svelte @@ -0,0 +1,5 @@ +go to dashboard diff --git a/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/state.js b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/state.js new file mode 100644 index 000000000000..1cc97fae3184 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/data-sveltekit/preload-data/redirect-gate/state.js @@ -0,0 +1 @@ +export const state = { selected: false }; diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 8b5d6df581ee..46606fab0345 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1071,6 +1071,24 @@ test.describe('data-sveltekit attributes', () => { await expect(page).toHaveURL(offline_url); }); + test('preloaded redirect is not replayed on navigation', async ({ page }) => { + await page.goto('/data-sveltekit/preload-data/redirect-gate/start'); + + // preload /dashboard while its load redirects to /select + await page.locator('#gated').hover(); + await page.locator('#gated').dispatchEvent('touchstart'); + await Promise.all([ + page.waitForTimeout(100), // wait for preloading to start + page.waitForLoadState('networkidle') // wait for preloading to finish + ]); + + // navigating must re-run load rather than replay the cached redirect, + // otherwise the mutual redirect never resolves and loops + await page.locator('#gated').click(); + await expect(page.locator('h1')).toHaveText('dashboard'); + await expect(page).toHaveURL(/redirect-gate\/dashboard/); + }); + test('data-sveltekit-preload-data error does not block user navigation', async ({ page, context,