Repository: sveltejs/kit (js)

Issue:
Make it clearer when client-side load is re-fetching data because the server-side load didn't use its wrapped fetch function
### Describe the problem

When using an external SDK in a `load` function without telling it what `fetch` to use (or if it doesn't have the ability to be told what `fetch` to use), the network response is not serialized into the server-rendered DOM, and on the client-side, the browser's native `fetch` doesn't know to first look for these serialized responses before making a network call. This results in degraded performance, because the same request is then performed again in the browser.

The solution here is for people to call the SDK on the server in an endpoint (preferably a shadow endpoint for this page, which will make this very easy), or to make sure they only call it client-side in `onMount`, say. However, there's no currently obvious way for people to know that this undesirable thing is even happening.

### Describe the proposed solution

In dev mode, we could override the browser's `window.fetch` with our own version that we can configure to complain when it is called (and then call the underlying native `fetch`). We could enable this during the `load` for the initial hydration of the page.

For bonus points, the `load`'s `fetch` could call this patched `window.fetch` function in such a way as to tell it that it _shouldn't_ complain during this request. This would reduce false positives where there is (for whatever reason) a genuine reason to request additional data in the client's `load` that wasn't requested in the server's `load`, as long as it is done via the appropriate `fetch` function.

### Alternatives considered

We could just document this, but that doesn't seem very discoverable. If someone's doing something that's probably wrong, we would like to tell them about it, rather than assuming that they've read the documentation and are keeping this in the back of their mind all the time.

### Importance

nice to have

### Additional Information

This proposal arose from a conversation in the maintainers' chat about #3729.

Pull request:
warn if load uses `window.fetch`
closes #3731

### 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/twelve-snakes-tan.md b/.changeset/twelve-snakes-tan.md
new file mode 100644
index 000000000..db2860288
--- /dev/null
+++ b/.changeset/twelve-snakes-tan.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Warn if load uses window.fetch
diff --git a/documentation/docs/02-routing.md b/documentation/docs/02-routing.md
index 88d15a051..1b2dbdb2f 100644
--- a/documentation/docs/02-routing.md
+++ b/documentation/docs/02-routing.md
@@ -62,7 +62,7 @@ declare module '$lib/database' {
 	export const get: (id: string) => Promise<Item>;
 }
 
-// @filename: [id].d.ts
+// @filename: __types/[id].d.ts
 import type { RequestHandler as GenericRequestHandler } from '@sveltejs/kit';
 export type RequestHandler<Body = any> = GenericRequestHandler<{ id: string }, Body>;
 
@@ -70,7 +70,7 @@ export type RequestHandler<Body = any> = GenericRequestHandler<{ id: string }, B
 // ---cut---
 import db from '$lib/database';
 
-/** @type {import('./[id]').RequestHandler} */
+/** @type {import('./__types/[id]').RequestHandler} */
 export async function get({ params }) {
 	// `params.id` comes from [id].js
 	const item = await db.get(params.id);
@@ -158,7 +158,7 @@ declare module '$lib/database' {
 	export const create: (request: Request) => Promise<[Record<string, ValidationError>, Item]>;
 }
 
-// @filename: items.d.ts
+// @filename: __types/items.d.ts
 import type { RequestHandler as GenericRequestHandler } from '@sveltejs/kit';
 export type RequestHandler<Body = any> = GenericRequestHandler<{}, Body>;
 
@@ -166,7 +166,7 @@ export type RequestHandler<Body = any> = GenericRequestHandler<{}, Body>;
 // ---cut---
 import * as db from '$lib/database';
 
-/** @type {import('./items').RequestHandler} */
+/** @type {import('./__types/items').RequestHandler} */
 export async function get() {
 	const items = await db.list();
 
@@ -175,7 +175,7 @@ export async function get() {
 	};
 }
 
-/** @type {import('./items').RequestHandler} */
+/** @type {import('./__types/items').RequestHandler} */
 export async function post({ request }) {
 	const [errors, item] = await db.create(request);
 
diff --git a/documentation/docs/03-layouts.md b/documentation/docs/03-layouts.md
index 1e33a4f46..1dcb2edd1 100644
--- a/documentation/docs/03-layouts.md
+++ b/documentation/docs/03-layouts.md
@@ -196,7 +196,7 @@ src/routes/
 ```svelte
 /// file: src/routes/marx-brothers/[...path].svelte
 <script context="module">
-	/** @type {import('./[...path]').Load} */
+	/** @type {import('./__types/[...path]').Load} */
 	export function load({ params }) {
 		return {
 			status: 404,
diff --git a/documentation/docs/04-loading.md b/documentation/docs/04-loading.md
index a4dd42dea..2a92f5342 100644
--- a/documentation/docs/04-loading.md
+++ b/documentation/docs/04-loading.md
@@ -9,7 +9,7 @@ If the data for a page comes from its endpoint, you may not need a `load` functi
 ```html
 /// file: src/routes/blog/[slug].svelte
 <script context="module">
-	/** @type {import('./[slug]').Load} */
+	/** @type {import('./__types/[slug]').Load} */
 	export async function load({ params, fetch, session, stuff }) {
 		const url = `https://cms.example.com/article/${params.slug}.json`;
 		const response = await fetch(url);
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 9d21057fc..1807bb580 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -13,7 +13,7 @@
 		"@sveltejs/vite-plugin-svelte": "^1.0.0-next.44",
 		"chokidar": "^3.5.3",
 		"sade": "^1.7.4",
-		"vite": "^2.9.0"
+		"vite": "^2.9.9"
 	},
 	"devDependencies": {
 		"@playwright/test": "^1.21.0",
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 8ee1be9e0..b16fe69ce 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -48,6 +48,33 @@ function update_scroll_positions(index) {
 	scroll_positions[index] = scroll_state();
 }
 
+const fetch = window.fetch;
+let loading = 0;
+
+if (import.meta.env.DEV) {
+	let can_inspect_stack_trace = false;
+
+	const check_stack_trace = async () => {
+		const stack = /** @type {string} */ (new Error().stack);
+		can_inspect_stack_trace = stack.includes('check_stack_trace');
+	};
+
+	check_stack_trace();
+
+	window.fetch = (input, init) => {
+		const url = input instanceof Request ? input.url : input.toString();
+		const stack = /** @type {string} */ (new Error().stack);
+
+		const heuristic = can_inspect_stack_trace ? stack.includes('load_node') : loading;
+		if (heuristic) {
+			console.warn(
+				`Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://kit.svelte.dev/docs/loading#input-fetch`
+			);
+		}
+		return fetch(input, init);
+	};
+}
+
 /**
  * @param {{
  *   target: Element;
@@ -559,7 +586,18 @@ export function create_client({ target, session, base, trailing_slash }) {
 				});
 			}
 
-			const loaded = await module.load.call(null, load_input);
+			let loaded;
+
+			if (import.meta.env.DEV) {
+				try {
+					loading += 1;
+					loaded = await module.load.call(null, load_input);
+				} finally {
+					loading -= 1;
+				}
+			} else {
+				loaded = await module.load.call(null, load_input);
+			}
 
 			if (!loaded) {
 				throw new Error('load function must return a value');
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6dd4b5654..c0023caaf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -286,12 +286,12 @@ importers:
       tiny-glob: ^0.2.9
       typescript: ^4.6.4
       uvu: ^0.5.2
-      vite: ^2.9.0
+      vite: ^2.9.9
     dependencies:
-      '@sveltejs/vite-plugin-svelte': 1.0.0-next.44_svelte@3.44.2+vite@2.9.0
+      '@sveltejs/vite-plugin-svelte': 1.0.0-next.44_svelte@3.44.2+vite@2.9.9
       chokidar: 3.5.3
       sade: 1.7.4
-      vite: 2.9.0
+      vite: 2.9.9
     devDependencies:
       '@playwright/test': 1.21.0
       '@rollup/plugin-replace': 4.0.0_rollup@2.60.2
@@ -468,7 +468,7 @@ importers:
       shiki-twoslash: ^3.0.2
       svelte: ^3.43.0
       typescript: ~4.6.2
-      vite: ^2.9.0
+      vite: ^2.9.9
       vite-imagetools: ^4.0.3
     devDependencies:
       '@sveltejs/adapter-auto': link:../../packages/adapter-auto
@@ -483,7 +483,7 @@ importers:
       shiki-twoslash: 3.0.2
       svelte: 3.44.2
       typescript: 4.6.2
-      vite: 2.9.0
+      vite: 2.9.9
       vite-imagetools: 4.0.3
 
 packages:
@@ -1476,7 +1476,7 @@ packages:
       golden-fleece: 1.0.9
     dev: true
 
-  /@sveltejs/vite-plugin-svelte/1.0.0-next.44_svelte@3.44.2+vite@2.9.0:
+  /@sveltejs/vite-plugin-svelte/1.0.0-next.44_svelte@3.44.2+vite@2.9.9:
     resolution: {integrity: sha512-n+sssEWbzykPS447FmnNyU5GxEhrBPDVd0lxNZnxRGz9P6651LjjwAnISKr3CKgT9v8IybP8VD0n2i5XzbqExg==}
     engines: {node: ^14.13.1 || >= 16}
     peerDependencies:
@@ -1494,7 +1494,7 @@ packages:
       magic-string: 0.26.2
       svelte: 3.44.2
       svelte-hmr: 0.14.11_svelte@3.44.2
-      vite: 2.9.0
+      vite: 2.9.9
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -4173,8 +4173,8 @@ packages:
       thenify-all: 1.6.0
     dev: true
 
-  /nanoid/3.3.2:
-    resolution: {integrity: sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==}
+  /nanoid/3.3.4:
+    resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
@@ -4602,11 +4602,11 @@ packages:
     resolution: {integrity: sha512-me2dL+chJVb88zpE228MvA6wIRy1CuXxGTwI5hYe4DnSnXRbtJT+9ggRj+49kgHgs/AKMTKOt/EkTHSvQJmRXA==}
     dev: true
 
-  /postcss/8.4.12:
-    resolution: {integrity: sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==}
+  /postcss/8.4.14:
+    resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}
     engines: {node: ^10 || ^12 || >=14}
     dependencies:
-      nanoid: 3.3.2
+      nanoid: 3.3.4
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
@@ -6005,8 +6005,8 @@ packages:
       magic-string: 0.25.7
     dev: true
 
-  /vite/2.9.0:
-    resolution: {integrity: sha512-5NAnNqzPmZzJvrswZGeTS2JHrBGIzIWJA2hBTTMYuoBVEMh0xwE0b5yyIXFxf7F07hrK4ugX2LJ7q6t7iIbd4Q==}
+  /vite/2.9.9:
+    resolution: {integrity: sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==}
     engines: {node: '>=12.2.0'}
     hasBin: true
     peerDependencies:
@@ -6022,7 +6022,7 @@ packages:
         optional: true
     dependencies:
       esbuild: 0.14.29
-      postcss: 8.4.12
+      postcss: 8.4.14
       resolve: 1.22.0
       rollup: 2.60.2
     optionalDependencies:
diff --git a/sites/kit.svelte.dev/package.json b/sites/kit.svelte.dev/package.json
index 8f75148b2..9ed0572ef 100644
--- a/sites/kit.svelte.dev/package.json
+++ b/sites/kit.svelte.dev/package.json
@@ -20,7 +20,7 @@
 		"shiki-twoslash": "^3.0.2",
 		"svelte": "^3.43.0",
 		"typescript": "~4.6.2",
-		"vite": "^2.9.0",
+		"vite": "^2.9.9",
 		"vite-imagetools": "^4.0.3"
 	},
 	"type": "module"


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/window-fetch/data.json.js b/packages/kit/test/apps/basics/src/routes/load/window-fetch/data.json.js
new file mode 100644
index 000000000..31bc26ce0
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/window-fetch/data.json.js
@@ -0,0 +1,8 @@
+/** @type {import('./data.json').RequestHandler} */
+export function get() {
+	return {
+		body: {
+			answer: 42
+		}
+	};
+}
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/src/routes/load/window-fetch/index.svelte b/packages/kit/test/apps/basics/src/routes/load/window-fetch/index.svelte
new file mode 100644
index 000000000..00ec7e8b4
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/window-fetch/index.svelte
@@ -0,0 +1,18 @@
+<script context="module">
+	/** @type {import('./index').Load} */
+	export async function load({ url }) {
+		const res = await fetch(`${url.origin}/load/window-fetch/data.json`);
+		const { answer } = await res.json();
+
+		return {
+			props: { answer }
+		};
+	}
+</script>
+
+<script>
+	/** @type {number} */
+	export let answer;
+</script>
+
+<h1>{answer}</h1>
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index cf7086835..29d70b6a0 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1455,6 +1455,25 @@ test.describe.parallel('Load', () => {
 		expect(cookies.answer).toBe('42');
 		expect(cookies.doubled).toBe('84');
 	});
+
+	test('using window.fetch causes a warning', async ({ page, javaScriptEnabled }) => {
+		const warnings = [];
+
+		page.on('console', (msg) => {
+			if (msg.type() === 'warning') {
+				warnings.push(msg.text());
+			}
+		});
+
+		await page.goto('/load/window-fetch');
+		expect(await page.textContent('h1')).toBe('42');
+
+		if (javaScriptEnabled && process.env.DEV) {
+			expect(warnings).toContain(
+				'Loading http://localhost:3000/load/window-fetch/data.json using `window.fetch`. For best results, use the `fetch` that is passed to your `load` function: https://kit.svelte.dev/docs/loading#input-fetch'
+			);
+		}
+	});
 });
 
 test.describe.parallel('Method overrides', () => {