Repository: sveltejs/kit (js)

Issue:
Caching issue with fetch response: Incorrect behavior observed
### Describe the bug

We ran into this issue while we were trying to do two similar GET requests to our API but with different headers. It seems that the caching system it's using only the URL and the body to create the key. Could you consider adding headers, too? Thanks

### Reproduction

https://stackblitz.com/edit/sveltejs-kit-template-default-igyc9t?file=src%2Froutes%2F%2Bpage.js,src%2Froutes%2F%2Bpage.svelte&terminal=dev

You need to download the project to don't run into a CORS issue with the API.

### Logs

_No response_

### System Info

```Shell
System:
    OS: macOS 12.5
    CPU: (10) arm64 Apple M1 Pro
    Memory: 5.28 GB / 16.00 GB
    Shell: 3.5.1 - /opt/homebrew/bin/fish
  Binaries:
    Node: 16.18.0 - ~/.nvm/versions/node/v16.18.0/bin/node
    Yarn: 1.22.19 - /opt/homebrew/bin/yarn
    npm: 8.19.2 - ~/.nvm/versions/node/v16.18.0/bin/npm
  Browsers:
    Chrome: 109.0.5414.119
    Safari: 15.6
  npmPackages:
    @sveltejs/adapter-auto: ^1.0.0 => 1.0.2 
    @sveltejs/kit: ^1.0.0 => 1.3.1 
    svelte: ^3.54.0 => 3.55.1 
    vite: ^4.0.0 => 4.0.4
```


### Severity

annoyance

### Additional Information

_No response_

Pull request:
fix: cache mechanism for request with different headers
closes #8752 

### 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.
- [ ] 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
- [ ] 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. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`.


Patch:
diff --git a/.changeset/chilly-bears-change.md b/.changeset/chilly-bears-change.md
new file mode 100644
index 000000000..3265bc09e
--- /dev/null
+++ b/.changeset/chilly-bears-change.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-cloudflare': patch
+---
+
+fix: correctly check URL pathname with Cloudflare adapter
diff --git a/.changeset/silent-planes-smell.md b/.changeset/silent-planes-smell.md
new file mode 100644
index 000000000..df9173436
--- /dev/null
+++ b/.changeset/silent-planes-smell.md
@@ -0,0 +1,5 @@
+---
+"@sveltejs/kit": patch
+---
+
+fix: consider headers when constructing request hash
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b08e334a2..df1e8e939 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,6 +30,14 @@ Entry points to be aware of are:
 - [`packages/kit/src/exports/vite`](https://github.com/sveltejs/kit/tree/master/packages/kit/src/exports/vite) - for all the Vite plugin related stuff
 - [`packages/adapter-[platform]`](https://github.com/sveltejs/kit/tree/master/packages) - for the various SvelteKit-provided adapters
 
+## Good first issues
+
+If you're looking for an issue to tackle to get familiar with the codebase and test suite, the [**low hanging fruit**](https://github.com/sveltejs/kit/issues?q=is%3Aissue+is%3Aopen+label%3A%22low+hanging+fruit%22) label contains issues that ought to be relatively straightforward to fix. Check to see if a PR already exists for an issue before working on it!
+
+Issues that have a clear solution but which _may_ be slightly more involved have the [**ready to implement**](https://github.com/sveltejs/kit/issues?q=is%3Aissue+is%3Aopen+label%3A%22ready+to+implement%22) label.
+
+Issues with the [**soon**](https://github.com/sveltejs/kit/issues?q=is%3Aissue+is%3Aopen+milestone%3Asoon) milestone are higher priority than issues with the [**later**](https://github.com/sveltejs/kit/issues?q=is%3Aissue+is%3Aopen+milestone%3Alater+) label (though PRs for 'later' issues are still welcome, especially if you're affected by them).
+
 ## Testing
 
 Run `pnpm test` to run the tests from all subpackages. Browser tests live in subdirectories of `packages/kit/test` such as `packages/kit/test/apps/basics`.
@@ -58,15 +66,15 @@ If you would like to test local changes to Vite or another dependency, you can b
 
 ```jsonc
 {
-	// ...
-	"dependencies": {
-		"vite": "^3.1.0"
-	},
-	"pnpm": {
-		"overrides": {
-			"vite": "link:../path/to/vite/packages/vite"
-		}
-	}
+  // ...
+  "dependencies": {
+    "vite": "^4.0.0"
+  },
+  "pnpm": {
+    "overrides": {
+      "vite": "link:../path/to/vite/packages/vite"
+    }
+  }
 }
 ```
 
diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md
index b1e80fb15..e865bcc34 100644
--- a/documentation/docs/20-core-concepts/20-load.md
+++ b/documentation/docs/20-core-concepts/20-load.md
@@ -415,6 +415,10 @@ export function load({ locals }) {
 }
 ```
 
+> Make sure you're not catching the thrown redirect, which results in a noop.
+
+In the browser, you can also navigate programmatically outside of a `load` function using [`goto`](modules#$app-navigation-goto) from [`$app.navigation`](modules#$app-navigation).
+
 ## Promise unwrapping
 
 Top-level promises will be awaited, which makes it easy to return multiple promises without creating a waterfall:
diff --git a/documentation/docs/25-build-and-deploy/50-adapter-static.md b/documentation/docs/25-build-and-deploy/50-adapter-static.md
index d34bcc73b..0c688a506 100644
--- a/documentation/docs/25-build-and-deploy/50-adapter-static.md
+++ b/documentation/docs/25-build-and-deploy/50-adapter-static.md
@@ -137,7 +137,7 @@ export default {
 
 When operating in SPA mode, you can omit the [`prerender`](page-options#prerender) option from your root layout (or set it to `false`, its default value), and only pages that have the `prerender` option set will be prerendered at build time.
 
-SvelteKit will still crawl your app's entry points looking for prerenderable pages. If `svelte-kit build` fails because of pages that can't be loaded outside the browser, you can set `config.kit.prerender.entries` to `[]` to prevent this from happening. (Setting `config.kit.prerender.enabled` to `false` also has this effect, but would prevent the fallback page from being generated.)
+SvelteKit will still crawl your app's entry points looking for prerenderable pages. If `svelte-kit build` fails because of pages that can't be loaded outside the browser, you can set `config.kit.prerender.entries` to `[]` to prevent this from happening.
 
 You can also add turn off prerendering only to parts of your app, if you want other parts to be prerendered.
 
diff --git a/package.json b/package.json
index 6dd9a7d7e..c9236e32f 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
 		"tiny-glob": "^0.2.9",
 		"typescript": "^4.9.4"
 	},
-	"packageManager": "pnpm@7.26.1",
+	"packageManager": "pnpm@7.26.2",
 	"engines": {
 		"pnpm": "^7.0.0"
 	},
diff --git a/packages/adapter-cloudflare/CHANGELOG.md b/packages/adapter-cloudflare/CHANGELOG.md
index b2ce1922e..8186fecf5 100644
--- a/packages/adapter-cloudflare/CHANGELOG.md
+++ b/packages/adapter-cloudflare/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-cloudflare
 
+## 2.0.0
+
+### Major Changes
+
+- breaking: append `_headers` instead of overwriting ([#8693](https://github.com/sveltejs/kit/pull/8693))
+
 ## 1.1.0
 
 ### Minor Changes
diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js
index 2e79a8cb8..7779b3746 100644
--- a/packages/adapter-cloudflare/index.js
+++ b/packages/adapter-cloudflare/index.js
@@ -22,10 +22,6 @@ export default function () {
 
 			const relativePath = posix.relative(tmp, builder.getServerDirectory());
 
-			builder.log.info(
-				`adapter-cloudfare is writing its own _headers file. If you have your own, you should duplicate the headers contained in: ${dest}/_headers`
-			);
-
 			writeFileSync(
 				`${tmp}/manifest.js`,
 				`export const manifest = ${builder.generateManifest({ relativePath })};\n\n` +
@@ -37,7 +33,7 @@ export default function () {
 				JSON.stringify(get_routes_json(builder, written_files))
 			);
 
-			writeFileSync(`${dest}/_headers`, generate_headers(builder.config.kit.appDir));
+			writeFileSync(`${dest}/_headers`, generate_headers(builder.config.kit.appDir), { flag: 'a' });
 
 			builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, {
 				replace: {
@@ -119,5 +115,5 @@ function generate_headers(app_dir) {
 	Cache-Control: public, immutable, max-age=31536000
 	X-Robots-Tag: noindex
 # === END AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
-	`.trim();
+`.trimEnd();
 }
diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json
index 25cfcb7cf..cfcb5ae06 100644
--- a/packages/adapter-cloudflare/package.json
+++ b/packages/adapter-cloudflare/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-cloudflare",
-	"version": "1.1.0",
+	"version": "2.0.0",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-cloudflare/src/worker.js b/packages/adapter-cloudflare/src/worker.js
index 7f57140c1..9c643e547 100644
--- a/packages/adapter-cloudflare/src/worker.js
+++ b/packages/adapter-cloudflare/src/worker.js
@@ -9,6 +9,7 @@ const app_path = `/${manifest.appPath}/`;
 /** @type {import('worktop/cfw').Module.Worker<{ ASSETS: import('worktop/cfw.durable').Durable.Object }>} */
 const worker = {
 	async fetch(req, env, context) {
+		// @ts-ignore
 		await server.init({ env });
 		// skip cache if "cache-control: no-cache" in request
 		let pragma = req.headers.get('cache-control') || '';
@@ -17,7 +18,7 @@ const worker = {
 
 		let { pathname } = new URL(req.url);
 
-		// static assets
+		// generated files
 		if (pathname.startsWith(app_path)) {
 			res = await env.ASSETS.fetch(req);
 			if (!res.ok) return res;
@@ -36,26 +37,38 @@ const worker = {
 				}
 			});
 		} else {
-			// prerendered pages and index.html files
-			pathname = pathname.replace(/\/$/, '') || '/';
-
-			let file = pathname.substring(1);
+			// prerendered pages and /static files
 
 			try {
-				file = decodeURIComponent(file);
-			} catch (err) {
-				// ignore
+				pathname = decodeURIComponent(pathname);
+			} catch {
+				// ignore invalid URI
+			}
+
+			const stripped_pathname = pathname.replace(/\/$/, '');
+
+			let is_static_asset = false;
+			const filename = stripped_pathname.substring(1);
+			if (filename) {
+				is_static_asset =
+					manifest.assets.has(filename) || manifest.assets.has(filename + '/index.html');
 			}
 
-			if (
-				manifest.assets.has(file) ||
-				manifest.assets.has(file + '/index.html') ||
-				prerendered.has(pathname)
-			) {
+			const counterpart_route = pathname.at(-1) === '/' ? stripped_pathname : pathname + '/';
+
+			if (is_static_asset || prerendered.has(pathname)) {
 				res = await env.ASSETS.fetch(req);
+			} else if (counterpart_route && prerendered.has(counterpart_route)) {
+				res = new Response('', {
+					status: 308,
+					headers: {
+						location: counterpart_route
+					}
+				});
 			} else {
 				// dynamically-generated pages
 				res = await server.respond(req, {
+					// @ts-ignore
 					platform: { env, context, caches },
 					getClientAddress() {
 						return req.headers.get('cf-connecting-ip');
diff --git a/packages/adapter-cloudflare/tsconfig.json b/packages/adapter-cloudflare/tsconfig.json
index b5a3f7c9f..4a37ad69a 100644
--- a/packages/adapter-cloudflare/tsconfig.json
+++ b/packages/adapter-cloudflare/tsconfig.json
@@ -11,5 +11,5 @@
 			"@sveltejs/kit": ["../kit/types/index"]
 		}
 	},
-	"include": ["index.js", "placeholders.ts", "src/worker.ts"]
+	"include": ["index.js", "placeholders.ts", "src/worker.js"]
 }
diff --git a/packages/adapter-vercel/CHANGELOG.md b/packages/adapter-vercel/CHANGELOG.md
index e7bb137ad..14e05a283 100644
--- a/packages/adapter-vercel/CHANGELOG.md
+++ b/packages/adapter-vercel/CHANGELOG.md
@@ -1,5 +1,14 @@
 # @sveltejs/adapter-vercel
 
+## 1.0.6
+
+### Patch Changes
+
+- fix: add trailing slash -> no trailing slash redirect for prerendered pages ([#8766](https://github.com/sveltejs/kit/pull/8766))
+
+- Updated dependencies [[`438e4318`](https://github.com/sveltejs/kit/commit/438e4318e96f3e192e273b95d20ee8f590e99829)]:
+  - @sveltejs/kit@1.3.4
+
 ## 1.0.5
 
 ### Patch Changes
diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js
index 529225d7c..47d384ee3 100644
--- a/packages/adapter-vercel/index.js
+++ b/packages/adapter-vercel/index.js
@@ -193,16 +193,24 @@ function static_vercel_config(builder) {
 	}
 
 	for (const [path, page] of builder.prerendered.pages) {
-		if (path.endsWith('/') && path !== '/') {
+		let overrides_path = path.slice(1);
+
+		if (path !== '/') {
+			/** @type {string | undefined} */
+			let counterpart_route = path + '/';
+
+			if (path.endsWith('/')) {
+				counterpart_route = path.slice(0, -1);
+				overrides_path = path.slice(1, -1);
+			}
+
 			prerendered_redirects.push(
-				{ src: path, dest: path.slice(0, -1) },
-				{ src: path.slice(0, -1), status: 308, headers: { Location: path } }
+				{ src: path, dest: counterpart_route },
+				{ src: counterpart_route, status: 308, headers: { Location: path } }
 			);
-
-			overrides[page.file] = { path: path.slice(1, -1) };
-		} else {
-			overrides[page.file] = { path: path.slice(1) };
 		}
+
+		overrides[page.file] = { path: overrides_path };
 	}
 
 	return {
diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json
index b87b33985..159f4d45f 100644
--- a/packages/adapter-vercel/package.json
+++ b/packages/adapter-vercel/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-vercel",
-	"version": "1.0.5",
+	"version": "1.0.6",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index bd3fbd2cd..3a10c760c 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,33 @@
 # @sveltejs/kit
 
+## 1.3.5
+
+### Patch Changes
+
+- docs: fix typo ([#8790](https://github.com/sveltejs/kit/pull/8790))
+
+- fix: build error on layout with missing leaves ([#8792](https://github.com/sveltejs/kit/pull/8792))
+
+- fix: handle hash links with non-ASCII characters when navigating ([#8767](https://github.com/sveltejs/kit/pull/8767))
+
+## 1.3.4
+
+### Patch Changes
+
+- chore: bump devalue ([#8789](https://github.com/sveltejs/kit/pull/8789))
+
+## 1.3.3
+
+### Patch Changes
+
+- fix: forward `process.env` to child process ([#8777](https://github.com/sveltejs/kit/pull/8777))
+
+## 1.3.2
+
+### Patch Changes
+
+- fix: take base path into account when preloading code ([#8748](https://github.com/sveltejs/kit/pull/8748))
+
 ## 1.3.1
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index bbf858c0e..16345d444 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.3.1",
+	"version": "1.3.5",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -13,7 +13,7 @@
 		"@sveltejs/vite-plugin-svelte": "^2.0.0",
 		"@types/cookie": "^0.5.1",
 		"cookie": "^0.5.0",
-		"devalue": "^4.2.2",
+		"devalue": "^4.2.3",
 		"esm-env": "^1.0.0",
 		"kleur": "^4.1.5",
 		"magic-string": "^0.27.0",
diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js
index 4a1cd2600..fe99fd43c 100644
--- a/packages/kit/src/core/postbuild/analyse.js
+++ b/packages/kit/src/core/postbuild/analyse.js
@@ -55,9 +55,9 @@ async function analyse({ manifest_path, env }) {
 	for (const loader of manifest._.nodes) {
 		const node = await loader();
 
-		metadata.nodes.push({
+		metadata.nodes[node.index] = {
 			has_server_load: node.server?.load !== undefined
-		});
+		};
 	}
 
 	// analyse routes
diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js
index 06c32d8a4..9137b9f86 100644
--- a/packages/kit/src/core/sync/write_client_manifest.js
+++ b/packages/kit/src/core/sync/write_client_manifest.js
@@ -38,7 +38,6 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
 
 	/** @type {Map<import('types').PageNode, number>} */
 	const indices = new Map();
-
 	const nodes = manifest_data.nodes
 		.map((node, i) => {
 			indices.set(node, i);
@@ -64,6 +63,7 @@ export function write_client_manifest(kit, manifest_data, output, metadata) {
 					if (route.leaf) {
 						if (metadata) {
 							const i = /** @type {number} */ (indices.get(route.leaf));
+
 							leaf_has_server_load = metadata[i].has_server_load;
 						} else if (route.leaf.server) {
 							leaf_has_server_load = true;
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index dbe070aa3..7c48c8f6f 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -343,7 +343,7 @@ export function create_client({ target, base }) {
 			}
 
 			if (autoscroll) {
-				const deep_linked = url.hash && document.getElementById(url.hash.slice(1));
+				const deep_linked = url.hash && document.querySelector(decodeURIComponent(url.hash));
 				if (scroll) {
 					scrollTo(scroll.x, scroll.y);
 				} else if (deep_linked) {
@@ -984,7 +984,7 @@ export function create_client({ target, base }) {
 	function get_navigation_intent(url, invalidating) {
 		if (is_external_url(url, base)) return;
 
-		const path = decode_pathname(url.pathname.slice(base.length) || '/');
+		const path = get_url_path(url);
 
 		for (const route of routes) {
 			const params = route.exec(path);
@@ -998,6 +998,11 @@ export function create_client({ target, base }) {
 		}
 	}
 
+	/** @param {URL} url */
+	function get_url_path(url) {
+		return decode_pathname(url.pathname.slice(base.length) || '/');
+	}
+
 	/**
 	 * @param {{
 	 *   url: URL;
@@ -1175,7 +1180,9 @@ export function create_client({ target, base }) {
 			(entries) => {
 				for (const entry of entries) {
 					if (entry.isIntersecting) {
-						preload_code(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href).pathname);
+						preload_code(
+							get_url_path(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href))
+						);
 						observer.unobserve(entry.target);
 					}
 				}
@@ -1200,7 +1207,7 @@ export function create_client({ target, base }) {
 				if (priority <= options.preload_data) {
 					preload_data(/** @type {URL} */ (url));
 				} else if (priority <= options.preload_code) {
-					preload_code(/** @type {URL} */ (url).pathname);
+					preload_code(get_url_path(/** @type {URL} */ (url)));
 				}
 			}
 		}
@@ -1220,7 +1227,7 @@ export function create_client({ target, base }) {
 				}
 
 				if (options.preload_code === PRELOAD_PRIORITIES.eager) {
-					preload_code(/** @type {URL} */ (url).pathname);
+					preload_code(get_url_path(/** @type {URL} */ (url)));
 				}
 			}
 		}
diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js
index 90db3fe44..e73da8f2c 100644
--- a/packages/kit/src/runtime/client/fetcher.js
+++ b/packages/kit/src/runtime/client/fetcher.js
@@ -127,8 +127,19 @@ function build_selector(resource, opts) {
 
 	let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
 
-	if (opts?.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
-		selector += `[data-hash="${hash(opts.body)}"]`;
+	if (opts?.headers || opts?.body) {
+		/** @type {import('types').StrictBody[]} */
+		const values = [];
+
+		if (opts.headers) {
+			values.push([...new Headers(opts.headers)].join(','));
+		}
+
+		if (opts.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
+			values.push(opts.body);
+		}
+
+		selector += `[data-hash="${hash(...values)}"]`;
 	}
 
 	return selector;
diff --git a/packages/kit/src/runtime/hash.js b/packages/kit/src/runtime/hash.js
index 0056251d3..cfebacb38 100644
--- a/packages/kit/src/runtime/hash.js
+++ b/packages/kit/src/runtime/hash.js
@@ -1,19 +1,21 @@
 /**
  * Hash using djb2
- * @param {import('types').StrictBody} value
+ * @param {import('types').StrictBody[]} values
  */
-export function hash(value) {
+export function hash(...values) {
 	let hash = 5381;
 
-	if (typeof value === 'string') {
-		let i = value.length;
-		while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
-	} else if (ArrayBuffer.isView(value)) {
-		const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
-		let i = buffer.length;
-		while (i) hash = (hash * 33) ^ buffer[--i];
-	} else {
-		throw new TypeError('value must be a string or TypedArray');
+	for (const value of values) {
+		if (typeof value === 'string') {
+			let i = value.length;
+			while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
+		} else if (ArrayBuffer.isView(value)) {
+			const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
+			let i = buffer.length;
+			while (i) hash = (hash * 33) ^ buffer[--i];
+		} else {
+			throw new TypeError('value must be a string or TypedArray');
+		}
 	}
 
 	return (hash >>> 0).toString(36);
diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js
index 31af151ae..0c212fa9e 100644
--- a/packages/kit/src/runtime/server/page/load_data.js
+++ b/packages/kit/src/runtime/server/page/load_data.js
@@ -194,6 +194,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
 									? await stream_to_string(cloned_body)
 									: init?.body
 							),
+							request_headers: init?.headers,
 							response_body: body,
 							response: response
 						});
diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js
index 5b275b0c6..f176fdfb4 100644
--- a/packages/kit/src/runtime/server/page/serialize_data.js
+++ b/packages/kit/src/runtime/server/page/serialize_data.js
@@ -73,8 +73,19 @@ export function serialize_data(fetched, filter, prerendering = false) {
 		`data-url=${escape_html_attr(fetched.url)}`
 	];
 
-	if (fetched.request_body) {
-		attrs.push(`data-hash=${escape_html_attr(hash(fetched.request_body))}`);
+	if (fetched.request_headers || fetched.request_body) {
+		/** @type {import('types').StrictBody[]} */
+		const values = [];
+
+		if (fetched.request_headers) {
+			values.push([...new Headers(fetched.request_headers)].join(','));
+		}
+
+		if (fetched.request_body) {
+			values.push(fetched.request_body);
+		}
+
+		attrs.push(`data-hash="${hash(...values)}"`);
 	}
 
 	// Compute the time the response should be cached, taking into account max-age and age.
diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts
index ccd13373a..2bb6c8980 100644
--- a/packages/kit/src/runtime/server/page/types.d.ts
+++ b/packages/kit/src/runtime/server/page/types.d.ts
@@ -5,6 +5,7 @@ export interface Fetched {
 	url: string;
 	method: string;
 	request_body?: string | ArrayBufferView | null;
+	request_headers?: HeadersInit | undefined;
 	response_body: string;
 	response: Response;
 }
diff --git a/packages/kit/src/utils/fork.js b/packages/kit/src/utils/fork.js
index 1cffc54a3..58ed2dcb5 100644
--- a/packages/kit/src/utils/fork.js
+++ b/packages/kit/src/utils/fork.js
@@ -39,6 +39,7 @@ export function forked(module, callback) {
 			const child = child_process.fork(fileURLToPath(module), {
 				stdio: 'inherit',
 				env: {
+					...process.env,
 					SVELTEKIT_FORK: 'true'
 				},
 				serialization: 'advanced'
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 8a8103795..474cd297b 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -237,7 +237,7 @@ declare module '$app/navigation' {
 	 * Programmatically imports the code for routes that haven't yet been fetched.
 	 * Typically, you might call this to speed up subsequent navigation.
 	 *
-	 * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about.svelte`) or `/blog/*` (to match `src/routes/blog/[slug].svelte`).
+	 * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about/+page.svelte`) or `/blog/*` (to match `src/routes/blog/[slug]/+page.svelte`).
 	 *
 	 * Unlike `preloadData`, this won't call `load` functions.
 	 * Returns a Promise that resolves when the modules have been imported.
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 4d0f5823e..761d4aec9 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -506,7 +506,7 @@ export interface KitConfig {
 	typescript?: {
 		/**
 		 * A function that allows you to edit the generated `tsconfig.json`. You can mutate the config (recommended) or return a new one.
-		 * This is useful for — to example — extend a shared `tsconfig.json` in a monorepo root
+		 * This is useful for extending a shared `tsconfig.json` in a monorepo root, for example.
 		 * @default (config) => config
 		 */
 		config?: (config: Record<string, any>) => Record<string, any> | void;
@@ -1087,7 +1087,8 @@ export type ActionResult<
 /**
  * Creates an `HttpError` object with an HTTP status code and an optional message.
  * This object, if thrown during request handling, will cause SvelteKit to
- * return an error response without invoking `handleError`
+ * return an error response without invoking `handleError`.
+ * Make sure you're not catching the thrown error, which results in a noop.
  * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses). Must be in the range 400-599.
  * @param body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
  */
@@ -1110,6 +1111,7 @@ export interface HttpError {
 
 /**
  * Create a `Redirect` object. If thrown during request handling, SvelteKit will return a redirect response.
+ * Make sure you're not catching the thrown redirect, which results in a noop.
  * @param status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 300-308.
  * @param location The location to redirect to.
  */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a1b2e8931..48a6d69b4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -265,7 +265,7 @@ importers:
       '@types/sade': ^1.7.4
       '@types/set-cookie-parser': ^2.4.2
       cookie: ^0.5.0
-      devalue: ^4.2.2
+      devalue: ^4.2.3
       esm-env: ^1.0.0
       kleur: ^4.1.5
       magic-string: ^0.27.0
@@ -286,7 +286,7 @@ importers:
       '@sveltejs/vite-plugin-svelte': 2.0.0_svelte@3.55.1+vite@4.0.4
       '@types/cookie': 0.5.1
       cookie: 0.5.0
-      devalue: 4.2.2
+      devalue: 4.2.3
       esm-env: 1.0.0
       kleur: 4.1.5
       magic-string: 0.27.0
@@ -2098,8 +2098,8 @@ packages:
     resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
     engines: {node: '>=8'}
 
-  /devalue/4.2.2:
-    resolution: {integrity: sha512-Pkwd8qrI9O20VJ14fBNHu+on99toTNZFbgWRpZbC0zbDXpnE2WHYcrC1fHhMsF/3Ee+2yaW7vEujAT7fCYgqrA==}
+  /devalue/4.2.3:
+    resolution: {integrity: sha512-JG6Q248aN0pgFL57e3zqTVeFraBe+5W2ugvv1mLXsJP6YYIYJhRZhAl7QP8haJrqob6X10F9NEkuCvNILZTPeQ==}
     dev: false
 
   /diff/5.1.0:


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/anchor/+page.svelte b/packages/kit/test/apps/basics/src/routes/anchor/+page.svelte
index 0d606cd72..479a2083d 100644
--- a/packages/kit/test/apps/basics/src/routes/anchor/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/anchor/+page.svelte
@@ -1,5 +1,6 @@
 <h1>Welcome to a test project</h1>
 <a id="scroll-anchor" href="#last-anchor-2">Bottom of this page</a>
+<a id="non-ascii-anchor" href="/anchor/anchor#go-to-encöded">Anchor demo (non-ASCII)</a>
 <a id="first-anchor" href="/anchor/anchor#go-to-element">Anchor demo (first)</a>
 <div>Spacing</div>
 <a id="second-anchor" href="/anchor/anchor#go-to-element">Anchor demo (second)</a>
diff --git a/packages/kit/test/apps/basics/src/routes/anchor/anchor/+page.svelte b/packages/kit/test/apps/basics/src/routes/anchor/anchor/+page.svelte
index 57406e6d3..1289d74e6 100644
--- a/packages/kit/test/apps/basics/src/routes/anchor/anchor/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/anchor/anchor/+page.svelte
@@ -2,3 +2,6 @@
 <div style="height: 180vh; background-color: peru;">
 	<p id="go-to-element">The browser scrolls to me</p>
 </div>
+<div style="height: 180vh; background-color: tomato;">
+	<p id="go-to-encöded">The browser scrolls to me</p>
+</div>
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte
index 1148676b0..059f911b8 100644
--- a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte
@@ -1,1 +1,4 @@
 <a href="/load/fetch-cache-control/load-data">load-data</a>
+
+<a href="/load/fetch-cache-control/headers-diff">headers-diff</a>
+
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.js
new file mode 100644
index 000000000..b64c9e0a9
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.js
@@ -0,0 +1,18 @@
+export async function load({ fetch, url }) {
+	const r1 = await fetch(url.pathname, {
+		headers: {
+			'x-foo': 'a'
+		}
+	});
+
+	const r2 = await fetch(url.pathname, {
+		headers: {
+			'x-foo': 'b'
+		}
+	});
+
+	return {
+		a: r1.json(),
+		b: r2.json()
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.svelte
new file mode 100644
index 000000000..8aa12f6b0
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.svelte
@@ -0,0 +1,7 @@
+<script>
+	export let data;
+</script>
+
+<a href="/load/fetch-cache-control">fetch-cache-control</a>
+
+<h2>{data.a.foo} / {data.b.foo}</h2>
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+server.js
new file mode 100644
index 000000000..5b181f409
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+server.js
@@ -0,0 +1,12 @@
+import { json } from '@sveltejs/kit';
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET({ request, setHeaders }) {
+	setHeaders({
+		'cache-control': 'public, max-age=7'
+	});
+
+	return json({
+		foo: request.headers.get('x-foo')
+	});
+}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 3a806e066..a2403e611 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -193,6 +193,25 @@ test.describe('Load', () => {
 		expect(did_request_data).toBe(false);
 	});
 
+	test('do not use cache if headers are different', async ({ page, clicknav }) => {
+		await page.goto('/load/fetch-cache-control/headers-diff');
+
+		// 1. We expect the right data
+		expect(await page.textContent('h2')).toBe('a / b');
+
+		// 2. Change to another route (client side)
+		await clicknav('[href="/load/fetch-cache-control"]');
+
+		// 3. Come back to the original page (client side)
+		const requests = [];
+		page.on('request', (request) => requests.push(request));
+		await clicknav('[href="/load/fetch-cache-control/headers-diff"]');
+
+		// 4. We expect the same data and no new request because it was cached.
+		expect(await page.textContent('h2')).toBe('a / b');
+		expect(requests).toEqual([]);
+	});
+
 	if (process.env.DEV) {
 		test('using window.fetch causes a warning', async ({ page, baseURL }) => {
 			await Promise.all([
diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js
index 42028b930..f92e08549 100644
--- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js
+++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js
@@ -214,6 +214,16 @@ test.describe('Scrolling', () => {
 		expect(await in_view('#go-to-element')).toBe(true);
 	});
 
+	test('url-supplied non-ascii anchor works on navigation to page', async ({
+		page,
+		in_view,
+		clicknav
+	}) => {
+		await page.goto('/anchor');
+		await clicknav('#non-ascii-anchor');
+		expect(await in_view('#go-to-encöded')).toBe(true);
+	});
+
 	test('url-supplied anchor works when navigated from scrolled page', async ({
 		page,
 		clicknav,
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index 748253030..a375d7621 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -164,8 +164,7 @@ test.describe('Errors', () => {
 		expect(await response.text()).toMatch('PUT method not allowed');
 	});
 
-	// TODO re-enable this if https://github.com/vitejs/vite/issues/7046 is implemented
-	test.skip('error evaluating module', async ({ request }) => {
+	test('error evaluating module', async ({ request }) => {
 		const response = await request.get('/errors/init-error-endpoint');
 
 		expect(response.status()).toBe(500);
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 8c477321d..e67b827bc 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -238,10 +238,10 @@ test.describe('Load', () => {
 			const payload_b = '{"status":200,"statusText":"","headers":{},"body":"Y"}';
 			// by the time JS has run, hydration will have nuked these scripts
 			const script_contents_a = await page.innerHTML(
-				'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="3t25"]'
+				'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="1vn6nlx"]'
 			);
 			const script_contents_b = await page.innerHTML(
-				'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="3t24"]'
+				'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="1vn6nlw"]'
 			);
 
 			expect(script_contents_a).toBe(payload_a);
diff --git a/packages/kit/test/apps/options/source/pages/preloading/+page.svelte b/packages/kit/test/apps/options/source/pages/preloading/+page.svelte
index 10d9cdce5..81aee0ffd 100644
--- a/packages/kit/test/apps/options/source/pages/preloading/+page.svelte
+++ b/packages/kit/test/apps/options/source/pages/preloading/+page.svelte
@@ -1,1 +1,2 @@
 <a data-sveltekit-preload-data href="/path-base/preloading/preloaded">click me</a>
+<a data-sveltekit-preload-code href="/path-base/preloading/code">click me 2</a>
diff --git a/packages/kit/test/apps/options/source/pages/preloading/code/+page.svelte b/packages/kit/test/apps/options/source/pages/preloading/code/+page.svelte
new file mode 100644
index 000000000..0e1d4dcf4
--- /dev/null
+++ b/packages/kit/test/apps/options/source/pages/preloading/code/+page.svelte
@@ -0,0 +1,1 @@
+<h1>Should preload my code</h1>
diff --git a/packages/kit/test/apps/options/test/test.js b/packages/kit/test/apps/options/test/test.js
index c79e42758..026d847f4 100644
--- a/packages/kit/test/apps/options/test/test.js
+++ b/packages/kit/test/apps/options/test/test.js
@@ -242,6 +242,33 @@ test.describe('trailingSlash', () => {
 		await app.goto('/path-base/preloading/preloaded');
 		expect(requests).toEqual([]);
 	});
+
+	test('accounts for base path when running data-sveltekit-preload-code', async ({
+		page,
+		javaScriptEnabled
+	}) => {
+		if (!javaScriptEnabled) return;
+
+		await page.goto('/path-base/preloading');
+
+		/** @type {string[]} */
+		let requests = [];
+		page.on('request', (r) => requests.push(new URL(r.url()).pathname));
+
+		await page.hover('a[href="/path-base/preloading/code"]');
+		await page.waitForTimeout(100);
+
+		// svelte request made is environment dependent
+		if (process.env.DEV) {
+			expect(requests.filter((req) => req.endsWith('.svelte')).length).toBe(1);
+		} else {
+			expect(requests.filter((req) => req.endsWith('.js')).length).toBeGreaterThan(0);
+		}
+
+		requests = [];
+		await page.click('a[href="/path-base/preloading/code"]');
+		expect(requests).toEqual([]);
+	});
 });
 
 if (!process.env.DEV) {