Repository: sveltejs/kit (js)

Issue:
fetch() unexpected behavor when encountering a CORS issue on client side
### Describe the bug

If SvelteKit's `fetch()` is called against a URL that causes a CORS error in a web browser, the resulting behaviour might be unexpected for a developer. The behaviour is also naturally different on client-side and server-side rendering, where SSR `fetch()` does not suffer from CORS issues.

- A CORS error does not return a response in `fetch()` but throws an exception. Developers relying on `response.ok` pattern to check the validity of the `fetch` result are caught off guard.
- This is a tough nut to crack, because it is rooted deeply in the web browser security model.
- The behaviour is unexpected because you need to work around using `try ... catch` instead of `response.ok` attribute to check the error.
- Although something must "have gone wrong before" this occurs, the unexpected behaviour is common, because many times HTTP 404 or HTTP 500 responses lack CORS headers on them. If a developer naively checks for `response.status === 404` the check might fail in a web browser, assuming a low quality third party API. This can happen if a developer is using `fetch()` in `load()`, `get()` or similar with third-party APIs. This is especially bad, when SSR renders different pages (w/working error handling) than the client-side router.


### Reproduction

This issue cannot be demostrated on StackBlitz (node.new) because it is related to low-level network primitives.

Here is an example one can throw in their `load()` function:

```typescript

            try {
                // causes CORS error because lack of CORS headers in 404
                resp = await fetch("https://example.com/random");
            } catch(e) {
                // A web browser throws TypeError: Failed to fetch
                // but happens only on client-side.
                // The exception is hard to distinguish from
                // any other exception, because it lacks metadata
                // (class name, attributes).
                console.error("Received", e)
                // Temporary workaround
                resp = {ok: false, statusText: e.message};
            }

	let error, meta;
	if (resp.ok) {
		meta = await resp.json();
		error = null;
	} else {
		meta = {};
		error = resp.statusText;
	}
```

### Logs

You will see this in the web developer console:

```
Access to fetch at 'https://example.com/random' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

However I am not sure if this information is accessible within client-side JavaScript.

### System Info

```shell
System:
    OS: macOS 12.3.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 902.50 MB / 64.00 GB
    Shell: 5.8.1 - /opt/homebrew/bin/zsh
  Binaries:
    Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node
    Yarn: 1.22.15 - ~/.nvm/versions/node/v16.14.2/bin/yarn
    npm: 8.7.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
  Browsers:
    Chrome: 101.0.4951.64
    Firefox: 100.0.2
    Safari: 15.4
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.34 
    @sveltejs/kit: next => 1.0.0-next.308 
    svelte: ^3.46.0 => 3.46.6
```


### Severity

annoyance

### Additional Information

I am not sure what would be the best way to address the issue. 

If this issue is not fixable on a framework level, I suggest this issue is highlighted in
- tutorial
- examples
- documentation
- etc. 

and developers are educated that `fetch()` may throw an exception, besides returning error responses.

There are likely to be other similar low-level network issues in a web browser that needs to be handled via catch in a `fetch()` caller. 

Pull request:
simulate CORS errors in server-side fetch
closes #5074, by increasing the fidelity of the server-side `fetch` implementation passed to `load`. If a request is made for an external resource, and it doesn't respond with an appropriate `access-control-allow-origin` header, it will fail the same way that the browser's `fetch` fails (albeit with a different error message, since there's no standardisation here). 

If the request was made with `no-cors`, we return a response with an empty body.

### 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/breezy-ladybugs-dance.md b/.changeset/breezy-ladybugs-dance.md
new file mode 100644
index 000000000..c15821bb5
--- /dev/null
+++ b/.changeset/breezy-ladybugs-dance.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] change Navigation type to include from/to.params and from/to.routeId
diff --git a/.changeset/green-kiwis-roll.md b/.changeset/green-kiwis-roll.md
new file mode 100644
index 000000000..b5c953e6b
--- /dev/null
+++ b/.changeset/green-kiwis-roll.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] prevent server-side fetch from reading files with # character in the filename
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 57a27c0eb..49627d8ce 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -88,6 +88,7 @@
     "breezy-games-count",
     "breezy-grapes-smash",
     "breezy-hairs-greet",
+    "breezy-ladybugs-dance",
     "breezy-mayflies-yawn",
     "breezy-olives-battle",
     "breezy-onions-remember",
@@ -387,6 +388,7 @@
     "great-queens-rule",
     "great-seahorses-fry",
     "green-garlics-warn",
+    "green-kiwis-roll",
     "green-masks-confess",
     "green-mayflies-shave",
     "green-spoons-count",
@@ -1000,6 +1002,7 @@
     "sour-waves-return",
     "spicy-apples-return",
     "spicy-buckets-draw",
+    "spicy-games-trade",
     "spicy-geese-accept",
     "spicy-glasses-promise",
     "spicy-jeans-rest",
diff --git a/.changeset/red-tomatoes-leave.md b/.changeset/red-tomatoes-leave.md
new file mode 100644
index 000000000..f4d0d327a
--- /dev/null
+++ b/.changeset/red-tomatoes-leave.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] simulate CORS errors in server-side fetch
diff --git a/.changeset/spicy-games-trade.md b/.changeset/spicy-games-trade.md
new file mode 100644
index 000000000..bf1225a0a
--- /dev/null
+++ b/.changeset/spicy-games-trade.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Warn about incorrect data-sveltekit-x values
diff --git a/documentation/docs/09-link-options.md b/documentation/docs/09-link-options.md
index b77ab71f7..59ba993b0 100644
--- a/documentation/docs/09-link-options.md
+++ b/documentation/docs/09-link-options.md
@@ -64,3 +64,11 @@ To disable any of these options inside an element where they have been enabled,
 	</div>
 </div>
 ```
+
+To apply an attribute to an element conditionally, do this:
+
+```html
+<div data-sveltekit-reload={shouldReload ? '' : 'off'}>
+```
+
+> This works because in HTML, `<element attribute>` is equivalent to `<element attribute="">`
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index af7d76704..a600c6b48 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,19 @@
 # @sveltejs/kit
 
+## 1.0.0-next.467
+
+### Patch Changes
+
+- [breaking] prevent server-side fetch from reading files with # character in the filename ([#6549](https://github.com/sveltejs/kit/pull/6549))
+
+## 1.0.0-next.466
+
+### Patch Changes
+
+- [breaking] change Navigation type to include from/to.params and from/to.routeId ([#6552](https://github.com/sveltejs/kit/pull/6552))
+
+* Warn about incorrect data-sveltekit-x values ([#6546](https://github.com/sveltejs/kit/pull/6546))
+
 ## 1.0.0-next.465
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index bf0b06ed6..d2ab0ddba 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.465",
+	"version": "1.0.0-next.467",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 1e61ce2cc..f866a04c1 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -145,7 +145,8 @@ export function create_client({ target, base, trailing_slash }) {
 			const url = new URL(location.href);
 
 			invalidating = Promise.resolve().then(async () => {
-				await update(url, []);
+				const intent = get_navigation_intent(url);
+				await update(intent, url, []);
 
 				invalidating = null;
 				force_invalidation = false;
@@ -200,14 +201,13 @@ export function create_client({ target, base, trailing_slash }) {
 
 	/**
 	 * Returns `true` if update completes, `false` if it is aborted
+	 * @param {import('./types').NavigationIntent | undefined} intent
 	 * @param {URL} url
 	 * @param {string[]} redirect_chain
 	 * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
 	 * @param {() => void} [callback]
 	 */
-	async function update(url, redirect_chain, opts, callback) {
-		const intent = get_navigation_intent(url);
-
+	async function update(intent, url, redirect_chain, opts, callback) {
 		const current_token = (token = {});
 		let navigation_result = intent && (await load_route(intent));
 
@@ -399,7 +399,15 @@ export function create_client({ target, base, trailing_slash }) {
 		}
 
 		/** @type {import('types').Navigation} */
-		const navigation = { from: null, to: new URL(location.href), type: 'load' };
+		const navigation = {
+			from: null,
+			to: add_url_properties('to', {
+				params: current.params,
+				routeId: current.route?.id ?? null,
+				url: new URL(location.href)
+			}),
+			type: 'load'
+		};
 		callbacks.after_navigate.forEach((fn) => fn(navigation));
 
 		started = true;
@@ -413,7 +421,7 @@ export function create_client({ target, base, trailing_slash }) {
 	 *   branch: Array<import('./types').BranchNode | undefined>;
 	 *   status: number;
 	 *   error: HttpError | Error | null;
-	 *   routeId: string | null;
+	 *   route: import('types').CSRRoute | null;
 	 *   validation_errors?: Record<string, any> | null;
 	 * }} opts
 	 */
@@ -423,7 +431,7 @@ export function create_client({ target, base, trailing_slash }) {
 		branch,
 		status,
 		error,
-		routeId,
+		route,
 		validation_errors
 	}) {
 		const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
@@ -436,6 +444,7 @@ export function create_client({ target, base, trailing_slash }) {
 				params,
 				branch,
 				error,
+				route,
 				session_id
 			},
 			props: {
@@ -470,7 +479,7 @@ export function create_client({ target, base, trailing_slash }) {
 			result.props.page = {
 				error,
 				params,
-				routeId,
+				routeId: route && route.id,
 				status,
 				url,
 				// The whole page store is updated, but this way the object reference stays the same
@@ -846,7 +855,7 @@ export function create_client({ target, base, trailing_slash }) {
 									branch: branch.slice(0, j + 1).concat(error_loaded),
 									status,
 									error,
-									routeId: route.id
+									route
 								});
 							} catch (e) {
 								continue;
@@ -872,7 +881,7 @@ export function create_client({ target, base, trailing_slash }) {
 			branch,
 			status: 200,
 			error: null,
-			routeId: route.id
+			route
 		});
 	}
 
@@ -941,7 +950,7 @@ export function create_client({ target, base, trailing_slash }) {
 			branch: [root_layout, root_error],
 			status,
 			error,
-			routeId
+			route: null
 		});
 	}
 
@@ -1000,10 +1009,20 @@ export function create_client({ target, base, trailing_slash }) {
 	}) {
 		let should_block = false;
 
+		const intent = get_navigation_intent(url);
+
 		/** @type {import('types').Navigation} */
 		const navigation = {
-			from: current.url,
-			to: url,
+			from: add_url_properties('from', {
+				params: current.params,
+				routeId: current.route?.id ?? null,
+				url: current.url
+			}),
+			to: add_url_properties('to', {
+				params: intent?.params ?? null,
+				routeId: intent?.route.id ?? null,
+				url
+			}),
 			type
 		};
 
@@ -1034,6 +1053,7 @@ export function create_client({ target, base, trailing_slash }) {
 		}
 
 		await update(
+			intent,
 			url,
 			redirect_chain,
 			{
@@ -1153,7 +1173,11 @@ export function create_client({ target, base, trailing_slash }) {
 
 				/** @type {import('types').Navigation & { cancel: () => void }} */
 				const navigation = {
-					from: current.url,
+					from: add_url_properties('from', {
+						params: current.params,
+						routeId: current.route?.id ?? null,
+						url: current.url
+					}),
 					to: null,
 					type: 'unload',
 					cancel: () => (should_block = true)
@@ -1184,7 +1208,7 @@ export function create_client({ target, base, trailing_slash }) {
 			/** @param {Event} event */
 			const trigger_prefetch = (event) => {
 				const { url, options } = find_anchor(event);
-				if (url && options.prefetch === '') {
+				if (url && options.prefetch) {
 					if (is_external_url(url)) return;
 					prefetch(url);
 				}
@@ -1234,7 +1258,7 @@ export function create_client({ target, base, trailing_slash }) {
 				// 2. 'rel' attribute includes external
 				const rel = (a.getAttribute('rel') || '').split(/\s+/);
 
-				if (a.hasAttribute('download') || rel.includes('external') || options.reload === '') {
+				if (a.hasAttribute('download') || rel.includes('external') || options.reload) {
 					return;
 				}
 
@@ -1260,7 +1284,7 @@ export function create_client({ target, base, trailing_slash }) {
 
 				navigate({
 					url,
-					scroll: options.noscroll === '' ? scroll_state() : null,
+					scroll: options.noscroll ? scroll_state() : null,
 					keepfocus: false,
 					redirect_chain: [],
 					details: {
@@ -1379,7 +1403,7 @@ export function create_client({ target, base, trailing_slash }) {
 						  )
 						: original_error,
 					validation_errors,
-					routeId
+					route: routes.find((route) => route.id === routeId) ?? null
 				});
 			} catch (e) {
 				const error = normalize_error(e);
@@ -1434,3 +1458,37 @@ async function load_data(url, invalid) {
 
 	return server_data;
 }
+
+// TODO remove for 1.0
+const properties = [
+	'hash',
+	'href',
+	'host',
+	'hostname',
+	'origin',
+	'pathname',
+	'port',
+	'protocol',
+	'search',
+	'searchParams',
+	'toString',
+	'toJSON'
+];
+
+/**
+ * @param {'from' | 'to'} type
+ * @param {import('types').NavigationTarget} target
+ */
+function add_url_properties(type, target) {
+	for (const prop of properties) {
+		Object.defineProperty(target, prop, {
+			get() {
+				throw new Error(
+					`The navigation shape changed - ${type}.${prop} should now be ${type}.url.${prop}`
+				);
+			}
+		});
+	}
+
+	return target;
+}
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index f2f5a3617..ad59df44a 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -81,10 +81,11 @@ export interface DataNode {
 	uses: Uses;
 }
 
-export type NavigationState = {
+export interface NavigationState {
 	branch: Array<BranchNode | undefined>;
 	error: HttpError | Error | null;
 	params: Record<string, string>;
+	route: CSRRoute | null;
 	session_id: number;
 	url: URL;
-};
+}
diff --git a/packages/kit/src/runtime/client/utils.js b/packages/kit/src/runtime/client/utils.js
index 1f6a659b1..25d183dd3 100644
--- a/packages/kit/src/runtime/client/utils.js
+++ b/packages/kit/src/runtime/client/utils.js
@@ -27,16 +27,14 @@ export function find_anchor(event) {
 	/** @type {HTMLAnchorElement | SVGAElement | undefined} */
 	let a;
 
-	const options = {
-		/** @type {string | null} */
-		noscroll: null,
+	/** @type {boolean | null} */
+	let noscroll = null;
 
-		/** @type {string | null} */
-		prefetch: null,
+	/** @type {boolean | null} */
+	let prefetch = null;
 
-		/** @type {string | null} */
-		reload: null
-	};
+	/** @type {boolean | null} */
+	let reload = null;
 
 	for (const element of event.composedPath()) {
 		if (!(element instanceof Element)) continue;
@@ -46,22 +44,43 @@ export function find_anchor(event) {
 			a = /** @type {HTMLAnchorElement | SVGAElement} */ (element);
 		}
 
-		if (options.noscroll === null) {
-			options.noscroll = element.getAttribute('data-sveltekit-noscroll');
-		}
+		if (noscroll === null) noscroll = get_link_option(element, 'data-sveltekit-noscroll');
+		if (prefetch === null) prefetch = get_link_option(element, 'data-sveltekit-prefetch');
+		if (reload === null) reload = get_link_option(element, 'data-sveltekit-reload');
+	}
 
-		if (options.prefetch === null) {
-			options.prefetch = element.getAttribute('data-sveltekit-prefetch');
-		}
+	const url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
 
-		if (options.reload === null) {
-			options.reload = element.getAttribute('data-sveltekit-reload');
+	return {
+		a,
+		url,
+		options: {
+			noscroll,
+			prefetch,
+			reload
 		}
-	}
+	};
+}
 
-	const url = a && new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
+const warned = new WeakSet();
+
+/**
+ * @param {Element} element
+ * @param {string} attribute
+ */
+function get_link_option(element, attribute) {
+	const value = element.getAttribute(attribute);
+	if (value === null) return value;
+
+	if (value === '') return true;
+	if (value === 'off') return false;
+
+	if (__SVELTEKIT_DEV__ && !warned.has(element)) {
+		console.error(`Unexpected value for ${attribute} — should be "" or "off"`, element);
+		warned.add(element);
+	}
 
-	return { a, url, options };
+	return false;
 }
 
 /** @param {any} value */
diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js
index f67ca95b6..703179fe3 100644
--- a/packages/kit/src/runtime/server/page/fetch.js
+++ b/packages/kit/src/runtime/server/page/fetch.js
@@ -63,7 +63,7 @@ export function create_fetch({ event, options, state, route, prerender_default }
 			}
 		}
 
-		const resolved = resolve(event.url.pathname, requested.split('?')[0]);
+		const resolved = resolve(event.url.pathname, requested.split('?')[0]).replace(/#.+$/, '');
 
 		/** @type {Response} */
 		let response;
@@ -152,6 +152,8 @@ export function create_fetch({ event, options, state, route, prerender_default }
 				requested = event.url.protocol + requested;
 			}
 
+			const url = new URL(requested);
+
 			// external fetch
 			// allow cookie passthrough for "same-origin"
 			// if SvelteKit is serving my.domain.com:
@@ -161,10 +163,7 @@ export function create_fetch({ event, options, state, route, prerender_default }
 			// - sub.my.domain.com WILL receive cookies
 			// ports do not affect the resolution
 			// leading dot prevents mydomain.com matching domain.com
-			if (
-				`.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`) &&
-				opts.credentials !== 'omit'
-			) {
+			if (`.${url.hostname}`.endsWith(`.${event.url.hostname}`) && opts.credentials !== 'omit') {
 				const cookie = event.request.headers.get('cookie');
 				if (cookie) opts.headers.set('cookie', cookie);
 			}
@@ -176,6 +175,25 @@ export function create_fetch({ event, options, state, route, prerender_default }
 
 			const external_request = new Request(requested, /** @type {RequestInit} */ (opts));
 			response = await options.hooks.externalFetch.call(null, external_request);
+
+			if (opts.mode === 'no-cors') {
+				response = new Response('', {
+					status: response.status,
+					statusText: response.statusText,
+					headers: response.headers
+				});
+			} else {
+				if (url.origin !== event.url.origin) {
+					const acao = response.headers.get('access-control-allow-origin');
+					if (!acao || (acao !== event.url.origin && acao !== '*')) {
+						throw new Error(
+							`CORS error: ${
+								acao ? 'Incorrect' : 'No'
+							} 'Access-Control-Allow-Origin' header is present on the requested resource`
+						);
+					}
+				}
+			}
 		}
 
 		const set_cookie = response.headers.get('set-cookie');
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 7eb746c2d..c09238532 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -219,11 +219,17 @@ export interface LoadEvent<
 	depends: (...deps: string[]) => void;
 }
 
+export interface NavigationTarget {
+	params: Record<string, string> | null;
+	routeId: string | null;
+	url: URL;
+}
+
 export type NavigationType = 'load' | 'unload' | 'link' | 'goto' | 'popstate';
 
 export interface Navigation {
-	from: URL | null;
-	to: URL | null;
+	from: NavigationTarget | null;
+	to: NavigationTarget | null;
 	type: NavigationType;
 	delta?: number;
 }


Test patch:
diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js
index b1131b608..6a40df117 100644
--- a/packages/kit/test/apps/basics/src/hooks.js
+++ b/packages/kit/test/apps/basics/src/hooks.js
@@ -37,7 +37,12 @@ export const handle = sequence(
 				? ({ html }) => html.replace('__REPLACEME__', 'Worked!')
 				: undefined
 		});
-		response.headers.append('set-cookie', 'name=SvelteKit; path=/; HttpOnly');
+
+		try {
+			// in some tests we fetch stuff with undici, and the headers are immutable.
+			// we can safely ignore it in those cases
+			response.headers.append('set-cookie', 'name=SvelteKit; path=/; HttpOnly');
+		} catch {}
 
 		return response;
 	}
diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
index 36560e6ef..70209b704 100644
--- a/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/after-navigate/a/+page.svelte
@@ -1,10 +1,10 @@
 <script>
 	import { afterNavigate } from '$app/navigation';
 
-	/** @type {URL | null} */
+	/** @type {import('@sveltejs/kit').NavigationTarget | null} */
 	let from;
 
-	/** @type {URL} */
+	/** @type {import('@sveltejs/kit').NavigationTarget} */
 	let to;
 
 	afterNavigate((navigation) => {
@@ -13,5 +13,5 @@
 	});
 </script>
 
-<h1>{from?.pathname} -> {to?.pathname}</h1>
+<h1>{from?.url.pathname} -> {to?.url.pathname}</h1>
 <a href="/after-navigate/b">/b</a>
diff --git a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
index c1e43e302..3db6a4022 100644
--- a/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/after-navigate/b/+page.svelte
@@ -1,10 +1,10 @@
 <script>
 	import { afterNavigate } from '$app/navigation';
 
-	/** @type {URL | null} */
+	/** @type {import('@sveltejs/kit').NavigationTarget | null} */
 	let from;
 
-	/** @type {URL} */
+	/** @type {import('@sveltejs/kit').NavigationTarget} */
 	let to;
 
 	afterNavigate((navigation) => {
@@ -13,5 +13,5 @@
 	});
 </script>
 
-<h1>{from?.pathname} -> {to?.pathname}</h1>
+<h1>{from?.url.pathname} -> {to?.url.pathname}</h1>
 <a href="/after-navigate/a">/a</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/+page.js b/packages/kit/test/apps/basics/src/routes/load/cors/+page.js
new file mode 100644
index 000000000..9fbd10aed
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/cors/+page.js
@@ -0,0 +1,8 @@
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch, url }) {
+	const res = await fetch(`http://localhost:${url.searchParams.get('port')}`);
+
+	return {
+		text: await res.text()
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/cors/+page.svelte
new file mode 100644
index 000000000..a5063216b
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/cors/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>{@html data.text}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.js b/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.js
new file mode 100644
index 000000000..70614cbbc
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.js
@@ -0,0 +1,10 @@
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch, url }) {
+	const res = await fetch(`http://localhost:${url.searchParams.get('port')}`, {
+		mode: 'no-cors'
+	});
+
+	return {
+		text: await res.text()
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.svelte
new file mode 100644
index 000000000..d689a601e
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/cors/no-cors/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>result: {@html data.text}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js b/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js
new file mode 100644
index 000000000..df92a5908
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.js
@@ -0,0 +1,8 @@
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch }) {
+	const res = await fetch('/load/assets/a#b.txt');
+
+	return {
+		status: res.status
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.svelte
new file mode 100644
index 000000000..80f6ef362
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/static-file-with-hash/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>status: {data.status}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte b/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
index 706de9b32..c15660dbf 100644
--- a/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
+++ b/packages/kit/test/apps/basics/src/routes/store/navigating/+layout.svelte
@@ -11,7 +11,7 @@
 <div id="nav-status">
 	{#if $navigating}
 		<p id="navigating">
-			navigating from {$navigating.from.pathname} to {$navigating.to.pathname} ({$navigating.type})
+			navigating from {$navigating.from.url.pathname} to {$navigating.to.url.pathname} ({$navigating.type})
 		</p>
 	{:else}
 		<p id="not-navigating">not currently navigating</p>
diff --git a/packages/kit/test/apps/basics/static/load/assets/a#b.txt b/packages/kit/test/apps/basics/static/load/assets/a#b.txt
new file mode 100644
index 000000000..f86c02590
--- /dev/null
+++ b/packages/kit/test/apps/basics/static/load/assets/a#b.txt
@@ -0,0 +1,1 @@
+nope
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index 9cf989154..aa4ba7b8b 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -249,7 +249,12 @@ test.describe('Load', () => {
 		request
 	}) => {
 		const response = await request.get('/errors/error-in-layout');
-		expect(await response.text()).toContain('Error: 500');
+		expect(await response.text()).toContain('Error: 404');
+	});
+
+	test('fetch does not load a file with a # character', async ({ request }) => {
+		const response = await request.get('/load/static-file-with-hash');
+		expect(await response.text()).toContain('status: 404');
 	});
 });
 
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 4455f6423..d46e73f17 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -889,6 +889,23 @@ test.describe('Load', () => {
 
 		expect(await page.textContent('h1')).toBe('true');
 	});
+
+	test('CORS errors are simulated server-side', async ({ page, read_errors }) => {
+		const { port, close } = await start_server(async (req, res) => {
+			res.end('hello');
+		});
+
+		await page.goto(`/load/cors?port=${port}`);
+		expect(await page.textContent('h1')).toBe('500');
+		expect(read_errors(`/load/cors`)).toContain(
+			`Error: CORS error: No 'Access-Control-Allow-Origin' header is present on the requested resource`
+		);
+
+		await page.goto(`/load/cors/no-cors?port=${port}`);
+		expect(await page.textContent('h1')).toBe('result: ');
+
+		await close();
+	});
 });
 
 test.describe('Method overrides', () => {