Repository: sveltejs/kit (JavaScript)

Issue:
`beforeNavigate` runs twice if redirecting in the navigation targets `load`
### Describe the bug

Don't know if this is a bug or a feature... But if I have a before navigate in a route. And then I redirect in the load function on the route we are navigating to. The beforeNavigate runs twice in the original route.

### Reproduction

It's reproduced here:
https://stackblitz.com/edit/sveltejs-kit-template-default-5ed6za?file=src/app.html

I've made this example of the bug occuring.
If we are at the `/` route (Home) and then go to `/about` (About) "BEFORE" is only logged once.
But if we go back to `/` and then go to `/test` (Test) we are correctly redirected to `/about` but "BEFORE" is logged twice.
The console looks like this for the latter case:
```
BEFORE
LOAD IN TEST
BEFORE
```

### Logs

_No response_

### System Info

```Shell
System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 16.14.2 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 7.17.0 - /usr/local/bin/npm
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.87 
    @sveltejs/kit: next => 1.0.0-next.539 
    svelte: ^3.46.0 => 3.52.0 
    vite: ^3.1.0 => 3.2.3
```


### Severity

serious, but I can work around it

### Additional Information

This is quite a serious bug for how we use `beforeNavigate` in our application. But can be worked around


Discussion:
If we deem this behavior working as expected, we should at least introduce a `type: 'redirect'` so these events can easily be ignored.
I was talking to @kevmodrome in the Swedish Svelte discord. And he made me think that it was not a bug if you think about it. So I tested some more and printed out `to.route` as well. And that gave me this:
![image](https://user-images.githubusercontent.com/12641358/201112385-abd957d4-1942-4274-ae92-d0400397bc5f.png)

But a type that allows us to ignore it as you said would be nice.

<details>
  <summary>Summary of our discussion</summary>
  
  My argument for why it should be a bug and not a "feature" was that when I navigate to a route that checks in its load if a user is authenticated. And the user isn't authenticated, I might want to throw a redirect to another route. Then it would be weird if we would run the beforeNavigate in the original route.

His argument for why it was working as intended was that I never navigate from the original route because the load threw and "blocked" the target route from rendering. And therefore I'm still on the original route when the redirect is ran. We try to navigate again (to the redirect destination)

I understand his argument and why it happens now. But I don't know if it makes sense to run the beforeNavigate again.. Because the user/client has already said that it want to navigate away from the original route if that makes sense..
</details>
I actually think redirects probably shouldn't result in `beforeNavigate` callbacks firing again — conceptually, it isn't a new navigation event. (I read the collapsed discussion, but `beforeNavigate` isn't meant as a place to do auth checks — that stuff should happen in `load`)



Patch:
diff --git a/.changeset/hungry-tips-obey.md b/.changeset/hungry-tips-obey.md
new file mode 100644
index 000000000000..f59b225de9c7
--- /dev/null
+++ b/.changeset/hungry-tips-obey.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] don't run beforeNavigate during redirects
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index e871bbf39177..ed26e5440545 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1066,7 +1066,10 @@ export function create_client({ target, base, trailing_slash }) {
 			}
 		};
 
-		callbacks.before_navigate.forEach((fn) => fn(cancellable));
+		if (!navigating) {
+			// Don't run the event during redirects
+			callbacks.before_navigate.forEach((fn) => fn(cancellable));
+		}
 
 		return should_block ? null : navigation;
 	}


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte b/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte
index 829056d1fb85..402bc60cf9fa 100644
--- a/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/before-navigate/prevent-navigation/+page.svelte
@@ -1,18 +1,21 @@
 <script>
 	import { beforeNavigate } from '$app/navigation';
 
-	let triggered = false;
+	let times_triggered = 0;
 	let unload = false;
 	let navigation_type;
-	beforeNavigate(({ cancel, type, willUnload }) => {
-		triggered = true;
+	beforeNavigate(({ cancel, type, willUnload, to }) => {
+		times_triggered++;
 		unload = willUnload;
 		navigation_type = type;
-		cancel();
+		if (!to?.route.id?.includes('redirect')) {
+			cancel();
+		}
 	});
 </script>
 
 <h1>prevent navigation</h1>
 <a href="/before-navigate/a">a</a>
+<a href="/before-navigate/redirect">redirect</a>
 <a href="https://google.de">external</a>
-<pre>{triggered} {unload} {navigation_type}</pre>
+<pre>{times_triggered} {unload} {navigation_type}</pre>
diff --git a/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js b/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js
new file mode 100644
index 000000000000..a5b9dcc347e4
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/before-navigate/redirect/+page.js
@@ -0,0 +1,5 @@
+import { redirect } from '@sveltejs/kit';
+
+export function load() {
+	throw redirect(307, '/before-navigate/prevent-navigation');
+}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 8a04dd861095..1d8e48f2b655 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -27,7 +27,7 @@ test.describe('beforeNavigate', () => {
 		await page.waitForLoadState('networkidle');
 
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
-		expect(await page.innerHTML('pre')).toBe('true false link');
+		expect(await page.innerHTML('pre')).toBe('1 false link');
 	});
 
 	test('prevents navigation to external', async ({ page, baseURL }) => {
@@ -38,7 +38,7 @@ test.describe('beforeNavigate', () => {
 
 		page.click('a[href="https://google.de"]'); // do NOT await this, promise only resolves after successful navigation, which never happens
 		await page.waitForTimeout(500);
-		await expect(page.locator('pre')).toHaveText('true true link');
+		await expect(page.locator('pre')).toHaveText('1 true link');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
 	});
 
@@ -46,14 +46,14 @@ test.describe('beforeNavigate', () => {
 		await page.goto('/before-navigate/prevent-navigation');
 		await app.goto('/before-navigate/a');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
-		expect(await page.innerHTML('pre')).toBe('true false goto');
+		expect(await page.innerHTML('pre')).toBe('1 false goto');
 	});
 
 	test('prevents external navigation triggered by goto', async ({ page, app, baseURL }) => {
 		await page.goto('/before-navigate/prevent-navigation');
 		await app.goto('https://google.de');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
-		expect(await page.innerHTML('pre')).toBe('true true goto');
+		expect(await page.innerHTML('pre')).toBe('1 true goto');
 	});
 
 	test('prevents navigation triggered by back button', async ({ page, app, baseURL }) => {
@@ -62,7 +62,7 @@ test.describe('beforeNavigate', () => {
 		await page.click('h1'); // The browsers block attempts to prevent navigation on a frame that's never had a user gesture.
 
 		await page.goBack();
-		expect(await page.innerHTML('pre')).toBe('true false popstate');
+		expect(await page.innerHTML('pre')).toBe('1 false popstate');
 		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
 	});
 
@@ -78,7 +78,17 @@ test.describe('beforeNavigate', () => {
 
 		await page.close({ runBeforeUnload: true });
 		expect(await type).toBe('beforeunload');
-		expect(await page.innerHTML('pre')).toBe('true true leave');
+		expect(await page.innerHTML('pre')).toBe('1 true leave');
+	});
+
+	test('is not triggered on redirect', async ({ page, baseURL }) => {
+		await page.goto('/before-navigate/prevent-navigation');
+
+		await page.click('[href="/before-navigate/redirect"]');
+		await page.waitForLoadState('networkidle');
+
+		expect(page.url()).toBe(baseURL + '/before-navigate/prevent-navigation');
+		expect(await page.innerHTML('pre')).toBe('1 false link');
 	});
 });