Repository: sveltejs/kit (js)

Issue:
Load function should re-run if it was prefetched with an error
### Describe the problem

I am having a situation where I am calling an API in my load functions and storing an access token and a refresh token in http cookies. When an endpoint returns 401 I redirect to a refresh token endpoint, update the cookies and redirect back to the original request. Anyways the problem happens when multiple requests are sent at the same time. Because the refresh token can only be used once. If multiple requests are sent with an expired access token, some of them will return 401 because the refresh token will no longer be valid. I can get around this in load functions by calling 1 endpoint first and the followed by others:
```
const res1 = await fetch(...);
const [res2, res3] = await fetch();
```

But what I can't control is when having links with prefetch enabled and a user hovers over multiple of them at the same time. If the token is expired, some of the load functions will fail (401). But the token will actually be refreshed in another request. So now when I open a prefetched link that failed, I get the 401 error although the token was refreshed. Does it make sense that a load function re-runs if an error was thrown during prefetching?


### Describe the proposed solution

If an error was thrown in a load function during prefetching, it should re-run wen visiting the actual page instead of showing the error.

### Alternatives considered

_No response_

### Importance

nice to have

### Additional Information

_No response_

Pull request description:
[fix] don't cache prefetch errors
Fixes #6709

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/fair-panthers-notice.md b/.changeset/fair-panthers-notice.md
new file mode 100644
index 000000000..12311421d
--- /dev/null
+++ b/.changeset/fair-panthers-notice.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+allow async function for `enhance` action parameter
diff --git a/.changeset/pink-jokes-call.md b/.changeset/pink-jokes-call.md
new file mode 100644
index 000000000..4f930b9d5
--- /dev/null
+++ b/.changeset/pink-jokes-call.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[feat] support throwing redirect in handle
diff --git a/.changeset/real-onions-pull.md b/.changeset/real-onions-pull.md
new file mode 100644
index 000000000..4b1873697
--- /dev/null
+++ b/.changeset/real-onions-pull.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Workaround for the DOM clobbering for use:enhance
diff --git a/.changeset/serious-buttons-push.md b/.changeset/serious-buttons-push.md
new file mode 100644
index 000000000..b10699320
--- /dev/null
+++ b/.changeset/serious-buttons-push.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] don't cache prefetch errors
diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js
index 9359f146b..6b1cbacb5 100644
--- a/packages/kit/src/runtime/app/forms.js
+++ b/packages/kit/src/runtime/app/forms.js
@@ -27,7 +27,8 @@ export function enhance(form, submit = () => {}) {
 	const fallback_callback = async ({ action, result, reset }) => {
 		if (result.type === 'success') {
 			if (reset !== false) {
-				form.reset();
+				// We call reset from the prototype to avoid DOM clobbering
+				HTMLFormElement.prototype.reset.call(form);
 			}
 			await invalidateAll();
 		}
@@ -49,9 +50,10 @@ export function enhance(form, submit = () => {}) {
 
 		const action = new URL(
 			// We can't do submitter.formAction directly because that property is always set
+			// We do cloneNode for avoid DOM clobbering - https://github.com/sveltejs/kit/issues/7593
 			event.submitter?.hasAttribute('formaction')
 				? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
-				: form.action
+				: /** @type {HTMLFormElement} */ (HTMLFormElement.prototype.cloneNode.call(form)).action
 		);
 
 		const data = new FormData(form);
@@ -67,13 +69,13 @@ export function enhance(form, submit = () => {}) {
 		const cancel = () => (cancelled = true);
 
 		const callback =
-			submit({
+			(await submit({
 				action,
 				cancel,
 				controller,
 				data,
 				form
-			}) ?? fallback_callback;
+			})) ?? fallback_callback;
 		if (cancelled) return;
 
 		/** @type {import('types').ActionResult} */
@@ -117,11 +119,13 @@ export function enhance(form, submit = () => {}) {
 		});
 	}
 
-	form.addEventListener('submit', handle_submit);
+	// @ts-expect-error
+	HTMLFormElement.prototype.addEventListener.call(form, 'submit', handle_submit);
 
 	return {
 		destroy() {
-			form.removeEventListener('submit', handle_submit);
+			// @ts-expect-error
+			HTMLFormElement.prototype.removeEventListener.call(form, 'submit', handle_submit);
 		}
 	};
 }
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index e7406a57e..583457a93 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -218,7 +218,16 @@ export function create_client({ target, base, trailing_slash }) {
 			throw new Error(`Attempted to prefetch a URL that does not belong to this app: ${url}`);
 		}
 
-		load_cache = { id: intent.id, promise: load_route(intent) };
+		load_cache = {
+			id: intent.id,
+			promise: load_route(intent).then((result) => {
+				if (result.type === 'loaded' && result.state.error) {
+					// Don't cache errors, because they might be transient
+					load_cache = null;
+				}
+				return result;
+			})
+		};
 
 		return load_cache.promise;
 	}
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index e88f2a30f..d0e9ab2dc 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -3,7 +3,7 @@ import { render_page } from './page/index.js';
 import { render_response } from './page/render.js';
 import { respond_with_error } from './page/respond_with_error.js';
 import { is_form_content_type } from '../../utils/http.js';
-import { GENERIC_ERROR, handle_fatal_error } from './utils.js';
+import { GENERIC_ERROR, handle_fatal_error, redirect_response } from './utils.js';
 import {
 	decode_pathname,
 	decode_params,
@@ -16,6 +16,7 @@ import { exec } from '../../utils/routing.js';
 import { INVALIDATED_HEADER, render_data } from './data/index.js';
 import { add_cookies_to_headers, get_cookies } from './cookie.js';
 import { create_fetch } from './fetch.js';
+import { Redirect } from '../control.js';
 
 /* global __SVELTEKIT_ADAPTER_NAME__ */
 
@@ -360,6 +361,9 @@ export async function respond(request, options, state) {
 
 		return response;
 	} catch (error) {
+		if (error instanceof Redirect) {
+			return redirect_response(error.status, error.location);
+		}
 		return handle_fatal_error(event, options, error);
 	}
 }


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
index 032b7ca9c..5ee9f544e 100644
--- a/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/actions/enhance/+page.svelte
@@ -14,6 +14,8 @@
 <pre class="formdata2">{JSON.stringify($page.form)}</pre>
 
 <form method="post" action="?/login" use:enhance>
+	<input type=hidden name="action" value="DOM clobbering" />
+	<input type=hidden name="reset" value="DOM clobbering" />
 	<input name="username" type="text" />
 	<button class="form1">Submit</button>
 	<button class="form1-register" formAction="?/register">Submit</button>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetched.json/+server.js b/packages/kit/test/apps/basics/src/routes/routing/prefetched.json/+server.js
deleted file mode 100644
index 04004e8da..000000000
--- a/packages/kit/test/apps/basics/src/routes/routing/prefetched.json/+server.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { json } from '@sveltejs/kit';
-
-export function GET() {
-	return json('prefetched');
-}
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetched/+page.js b/packages/kit/test/apps/basics/src/routes/routing/prefetched/+page.js
deleted file mode 100644
index 76a9f0c1c..000000000
--- a/packages/kit/test/apps/basics/src/routes/routing/prefetched/+page.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/** @type {import('@sveltejs/kit').Load} */
-export async function load({ fetch }) {
-	const message = await fetch('/routing/prefetched.json').then((r) => r.json());
-	return { message };
-}
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetched/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/prefetched/+page.svelte
deleted file mode 100644
index 65b84bb82..000000000
--- a/packages/kit/test/apps/basics/src/routes/routing/prefetched/+page.svelte
+++ /dev/null
@@ -1,6 +0,0 @@
-<script>
-	/** @type {import('./$types').PageData} */
-	export let data;
-</script>
-
-<h1>{data.message}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetched/hash-route/+page.js b/packages/kit/test/apps/basics/src/routes/routing/prefetched/hash-route/+page.js
deleted file mode 100644
index 81da9fc7a..000000000
--- a/packages/kit/test/apps/basics/src/routes/routing/prefetched/hash-route/+page.js
+++ /dev/null
@@ -1,6 +0,0 @@
-let load_calls = 0;
-
-export const load = () => {
-	load_calls += 1;
-	return { calls: load_calls };
-};
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetched/hash-route/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/prefetched/hash-route/+page.svelte
deleted file mode 100644
index dd0428be8..000000000
--- a/packages/kit/test/apps/basics/src/routes/routing/prefetched/hash-route/+page.svelte
+++ /dev/null
@@ -1,29 +0,0 @@
-<script>
-	import { onMount } from 'svelte';
-	import { page } from '$app/stores';
-
-	export let data;
-
-	const modal_contents = {
-		'please-dont-show-me': {
-			title: 'Oopsie'
-		},
-		'please-dont-show-me-jr': {
-			title: 'Oopsie Jr.'
-		}
-	};
-
-	let modal = undefined;
-
-	const show_modal = () => {
-		const hash = $page.url.hash.substring(1);
-		modal = modal_contents[hash];
-	};
-
-	onMount(show_modal);
-</script>
-
-<svelte:window on:popstate={show_modal} />
-
-<h1>{modal?.title ?? ''}</h1>
-<p>Loaded {data.calls} times.</p>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.js b/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.js
new file mode 100644
index 000000000..eed3de964
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.js
@@ -0,0 +1,9 @@
+let throw_error = false;
+
+/** @type {import('@sveltejs/kit').Load} */
+export async function load() {
+	throw_error = !throw_error;
+	if (throw_error) {
+		throw new Error('error');
+	}
+}
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.svelte
new file mode 100644
index 000000000..302a01f33
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetch-error/+page.svelte
@@ -0,0 +1,1 @@
+<p>hello</p>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js b/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js
index 76a9f0c1c..2c658c282 100644
--- a/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js
+++ b/packages/kit/test/apps/basics/src/routes/routing/prefetching/prefetched/+page.js
@@ -1,5 +1,5 @@
 /** @type {import('@sveltejs/kit').Load} */
 export async function load({ fetch }) {
-	const message = await fetch('/routing/prefetched.json').then((r) => r.json());
+	const message = await fetch('/routing/prefetching/prefetched.json').then((r) => r.json());
 	return { message };
 }
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 2b89f27f1..f0c84475d 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -666,8 +666,8 @@ test.describe('Prefetching', () => {
 		// also wait for network processing to complete, see
 		// https://playwright.dev/docs/network#network-events
 		await Promise.all([
-			page.waitForResponse(`${baseURL}/routing/prefetched.json`),
-			app.prefetch('/routing/prefetched')
+			page.waitForResponse(`${baseURL}/routing/prefetching/prefetched.json`),
+			app.prefetch('/routing/prefetching/prefetched')
 		]);
 
 		// svelte request made is environment dependent
@@ -679,10 +679,10 @@ test.describe('Prefetching', () => {
 			expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0);
 		}
 
-		expect(requests.includes(`${baseURL}/routing/prefetched.json`)).toBe(true);
+		expect(requests.includes(`${baseURL}/routing/prefetching/prefetched.json`)).toBe(true);
 
 		requests = [];
-		await app.goto('/routing/prefetched');
+		await app.goto('/routing/prefetching/prefetched');
 		expect(requests).toEqual([]);
 
 		try {
@@ -698,28 +698,36 @@ test.describe('Prefetching', () => {
 		page
 	}) => {
 		await page.goto('/routing/a');
-		await app.prefetch('/routing/prefetched/hash-route#please-dont-show-me');
-		await app.goto('/routing/prefetched/hash-route');
+		await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me');
+		await app.goto('/routing/prefetching/hash-route');
 		await expect(page.locator('h1')).not.toHaveText('Oopsie');
 	});
 
 	test('does not rerun load on calls to duplicate preload hash route', async ({ app, page }) => {
 		await page.goto('/routing/a');
 
-		await app.prefetch('/routing/prefetched/hash-route#please-dont-show-me');
-		await app.prefetch('/routing/prefetched/hash-route#please-dont-show-me');
-		await app.goto('/routing/prefetched/hash-route#please-dont-show-me');
+		await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me');
+		await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me');
+		await app.goto('/routing/prefetching/hash-route#please-dont-show-me');
 		await expect(page.locator('p')).toHaveText('Loaded 1 times.');
 	});
 
 	test('does not rerun load on calls to different preload hash route', async ({ app, page }) => {
 		await page.goto('/routing/a');
 
-		await app.prefetch('/routing/prefetched/hash-route#please-dont-show-me');
-		await app.prefetch('/routing/prefetched/hash-route#please-dont-show-me-jr');
-		await app.goto('/routing/prefetched/hash-route#please-dont-show-me');
+		await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me');
+		await app.prefetch('/routing/prefetching/hash-route#please-dont-show-me-jr');
+		await app.goto('/routing/prefetching/hash-route#please-dont-show-me');
 		await expect(page.locator('p')).toHaveText('Loaded 1 times.');
 	});
+
+	test('does rerun load when prefetch errored', async ({ app, page }) => {
+		await page.goto('/routing/a');
+
+		await app.prefetch('/routing/prefetching/prefetch-error');
+		await app.goto('/routing/prefetching/prefetch-error');
+		await expect(page.locator('p')).toHaveText('hello');
+	});
 });
 
 test.describe('Routing', () => {