Repository: sveltejs/kit
Pull request #6972 (2022-09-22 15:34:37)

[fix] unwrap promises for load returns on the client
### 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
- [ ] 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
- [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


Conduitry: What happens right now if any of these aren't promises? How much time do we waste in that (common) case?

What happens if one of the promises rejects? What sort of errors does the user see in their application?
gtm-nayan: > What happens right now if any of these aren't promises?

Without the PR it's equivalent to copying them over to the same key in a new object, with the PR the behavior for objects that aren't promises remain unchanged whereas objects that are promises are now resolved before being passed on.

> How much time do we waste in that (common) case? What happens if one of the promises rejects? What sort of errors does the user see in their application?

These I don't have a clear answer to either. The behaviour is already there on the server side for better or for the worse, and the intent for this PR is to make it consistent on the client.
I did change the implementation of unwrap_promises a bit, [ctx](https://discord.com/channels/457912077277855764/571775594002513921/1022525955304083476) but that could be done in a separate PR to keep things tidy if needed.

Patch:
diff --git a/.changeset/flat-dancers-sell.md b/.changeset/flat-dancers-sell.md
new file mode 100644
index 000000000..923d3e8cd
--- /dev/null
+++ b/.changeset/flat-dancers-sell.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Allow prerendered pages to link to non-prerenderable endpoints
diff --git a/.changeset/kind-adults-drum.md b/.changeset/kind-adults-drum.md
new file mode 100644
index 000000000..43356b864
--- /dev/null
+++ b/.changeset/kind-adults-drum.md
@@ -0,0 +1,6 @@
+---
+'@sveltejs/kit': patch
+'create-svelte': patch
+---
+
+[breaking] rename App.PageError to App.Error
diff --git a/.changeset/pre.json b/.changeset/pre.json
index fc05d8d1a..c0fac7a8b 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -329,6 +329,7 @@
     "five-tools-arrive",
     "five-turtles-rhyme",
     "flat-cameras-build",
+    "flat-dancers-sell",
     "flat-ducks-impress",
     "flat-eels-hear",
     "flat-insects-worry",
@@ -525,6 +526,7 @@
     "khaki-plants-compare",
     "khaki-socks-tan",
     "khaki-wolves-shout",
+    "kind-adults-drum",
     "kind-ducks-relate",
     "kind-steaks-bake",
     "kind-tools-shop",
@@ -923,6 +925,7 @@
     "selfish-avocados-develop",
     "selfish-crews-count",
     "selfish-dancers-happen",
+    "selfish-geckos-grin",
     "selfish-ligers-allow",
     "selfish-masks-rhyme",
     "selfish-owls-own",
@@ -1166,6 +1169,7 @@
     "tasty-geese-burn",
     "tasty-geese-hug",
     "tasty-islands-drive",
+    "tasty-lizards-provide",
     "tasty-squids-share",
     "tasty-zebras-guess",
     "ten-apricots-admire",
diff --git a/.changeset/selfish-geckos-grin.md b/.changeset/selfish-geckos-grin.md
new file mode 100644
index 000000000..f2deffabf
--- /dev/null
+++ b/.changeset/selfish-geckos-grin.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] handle SPA root data loading error
diff --git a/.changeset/silver-badgers-travel.md b/.changeset/silver-badgers-travel.md
new file mode 100644
index 000000000..ec3b7c4bc
--- /dev/null
+++ b/.changeset/silver-badgers-travel.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] unwrap promises for load function data on the client
diff --git a/.changeset/tasty-lizards-provide.md b/.changeset/tasty-lizards-provide.md
new file mode 100644
index 000000000..ce32d7dec
--- /dev/null
+++ b/.changeset/tasty-lizards-provide.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+chdir before attempting postinstall
diff --git a/documentation/docs/07-hooks.md b/documentation/docs/07-hooks.md
index 8ede315c3..c25f9b11a 100644
--- a/documentation/docs/07-hooks.md
+++ b/documentation/docs/07-hooks.md
@@ -136,14 +136,14 @@ The following can be added to `src/hooks.server.js` _and_ `src/hooks.client.js`:
 If an unexpected error is thrown during loading or rendering, this function will be called with the `error` and the `event`. This allows for two things:
 
 - you can log the error
-- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value becomes the value of `$page.error`. It defaults to `{ message: 'Not Found' }` in case of a 404 (you can detect them through `event.routeId` being `null`) and to `{ message: 'Internal Error' }` for everything else. To make this type-safe, you can customize the expected shape by declaring an `App.PageError` interface (which must include `message: string`, to guarantee sensible fallback behavior).
+- you can generate a custom representation of the error that is safe to show to users, omitting sensitive details like messages and stack traces. The returned value becomes the value of `$page.error`. It defaults to `{ message: 'Not Found' }` in case of a 404 (you can detect them through `event.routeId` being `null`) and to `{ message: 'Internal Error' }` for everything else. To make this type-safe, you can customize the expected shape by declaring an `App.Error` interface (which must include `message: string`, to guarantee sensible fallback behavior).
 
 The following code shows an example of typing the error shape as `{ message: string; code: string }` and returning it accordingly from the `handleError` functions:
 
 ```ts
 /// file: src/app.d.ts
 declare namespace App {
-	interface PageError {
+	interface Error {
 		message: string;
 		code: string;
 	}
diff --git a/package.json b/package.json
index 6109081fc..f40614d65 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
 		"turbo": "^1.3.4",
 		"typescript": "^4.8.2"
 	},
-	"packageManager": "pnpm@7.12.1",
+	"packageManager": "pnpm@7.12.2",
 	"engines": {
 		"pnpm": "^7.0.0"
 	},
diff --git a/packages/create-svelte/CHANGELOG.md b/packages/create-svelte/CHANGELOG.md
index 39629f296..c05a66e32 100644
--- a/packages/create-svelte/CHANGELOG.md
+++ b/packages/create-svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
 # create-svelte
 
+## 2.0.0-next.179
+
+### Patch Changes
+
+- [breaking] rename App.PageError to App.Error ([#6963](https://github.com/sveltejs/kit/pull/6963))
+
 ## 2.0.0-next.178
 
 ### Patch Changes
diff --git a/packages/create-svelte/package.json b/packages/create-svelte/package.json
index e52cc1d40..b850fc800 100644
--- a/packages/create-svelte/package.json
+++ b/packages/create-svelte/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "create-svelte",
-	"version": "2.0.0-next.178",
+	"version": "2.0.0-next.179",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/create-svelte/templates/default/src/app.d.ts b/packages/create-svelte/templates/default/src/app.d.ts
index 89fa05e74..f5675352f 100644
--- a/packages/create-svelte/templates/default/src/app.d.ts
+++ b/packages/create-svelte/templates/default/src/app.d.ts
@@ -8,7 +8,7 @@ declare namespace App {
 
 	// interface PageData {}
 
-	// interface PageError {}
+	// interface Error {}
 
 	// interface Platform {}
 }
diff --git a/packages/create-svelte/templates/skeleton/src/app.d.ts b/packages/create-svelte/templates/skeleton/src/app.d.ts
index 6f53deca8..8f4d63895 100644
--- a/packages/create-svelte/templates/skeleton/src/app.d.ts
+++ b/packages/create-svelte/templates/skeleton/src/app.d.ts
@@ -4,6 +4,6 @@
 declare namespace App {
 	// interface Locals {}
 	// interface PageData {}
-	// interface PageError {}
+	// interface Error {}
 	// interface Platform {}
 }
diff --git a/packages/create-svelte/templates/skeletonlib/src/app.d.ts b/packages/create-svelte/templates/skeletonlib/src/app.d.ts
index 2c2f9ec4f..20346fb23 100644
--- a/packages/create-svelte/templates/skeletonlib/src/app.d.ts
+++ b/packages/create-svelte/templates/skeletonlib/src/app.d.ts
@@ -6,6 +6,6 @@
 declare namespace App {
 	// interface Locals {}
 	// interface PageData {}
-	// interface PageError {}
+	// interface Error {}
 	// interface Platform {}
 }
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 394b0c3e9..d5bc9fc1f 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,25 @@
 # @sveltejs/kit
 
+## 1.0.0-next.500
+
+### Patch Changes
+
+- Allow prerendered pages to link to non-prerenderable endpoints ([#6977](https://github.com/sveltejs/kit/pull/6977))
+
+* [fix] handle SPA root data loading error ([#6918](https://github.com/sveltejs/kit/pull/6918))
+
+## 1.0.0-next.499
+
+### Patch Changes
+
+- [breaking] rename App.PageError to App.Error ([#6963](https://github.com/sveltejs/kit/pull/6963))
+
+## 1.0.0-next.498
+
+### Patch Changes
+
+- chdir before attempting postinstall ([#6969](https://github.com/sveltejs/kit/pull/6969))
+
 ## 1.0.0-next.497
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index ac7b8312a..a27de1cff 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.497",
+	"version": "1.0.0-next.500",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/postinstall.js b/packages/kit/postinstall.js
index 980337e5b..398eb37cd 100644
--- a/packages/kit/postinstall.js
+++ b/packages/kit/postinstall.js
@@ -4,12 +4,13 @@ import glob from 'tiny-glob/sync.js';
 import { load_config } from './src/core/config/index.js';
 import * as sync from './src/core/sync/sync.js';
 
+const cwd = process.env.INIT_CWD ?? process.cwd();
+process.chdir(cwd);
+
 const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
 
 const directories = [];
 
-const cwd = process.env.INIT_CWD ?? process.cwd();
-
 if (pkg.workspaces) {
 	for (const directory of pkg.workspaces) {
 		directories.push(...glob(directory, { cwd }).map((dir) => path.resolve(cwd, dir)));
diff --git a/packages/kit/src/core/sync/sync.js b/packages/kit/src/core/sync/sync.js
index 5949de8fa..ce7c8dccf 100644
--- a/packages/kit/src/core/sync/sync.js
+++ b/packages/kit/src/core/sync/sync.js
@@ -1,3 +1,4 @@
+import fs from 'fs';
 import path from 'path';
 import create_manifest_data from './create_manifest_data/index.js';
 import { write_client_manifest } from './write_client_manifest.js';
@@ -13,6 +14,16 @@ import { write_ambient } from './write_ambient.js';
  * @param {string} mode
  */
 export function init(config, mode) {
+	// TODO remove for 1.0
+	if (fs.existsSync('src/app.d.ts')) {
+		const content = fs.readFileSync('src/app.d.ts', 'utf-8');
+		if (content.includes('PageError')) {
+			throw new Error(
+				'App.PageError has been renamed to App.Error — please update your src/app.d.ts'
+			);
+		}
+	}
+
 	write_tsconfig(config.kit);
 	write_ambient(config.kit, mode);
 }
diff --git a/packages/kit/src/core/sync/write_client_manifest.js b/packages/kit/src/core/sync/write_client_manifest.js
index 89aa584a6..542e65d78 100644
--- a/packages/kit/src/core/sync/write_client_manifest.js
+++ b/packages/kit/src/core/sync/write_client_manifest.js
@@ -99,7 +99,7 @@ export function write_client_manifest(config, manifest_data, output) {
 			export const hooks = {
 				handleError: ${
 					hooks_file ? 'client_hooks.handleError || ' : ''
-				}(({ error }) => { console.error(error); return { message: 'Internal Error' }; }),
+				}(({ error }) => { console.error(error) }),
 			};
 		`)
 	);
diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js
index 0f1120e5f..b7265680f 100644
--- a/packages/kit/src/exports/vite/build/build_server.js
+++ b/packages/kit/src/exports/vite/build/build_server.js
@@ -74,7 +74,7 @@ export class Server {
 					get request() {
 						throw new Error('request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details');
 					}
-				}) ?? { message: event.routeId ? 'Internal Error' : 'Not Found' };
+				}) ?? { message: event.routeId != null ? 'Internal Error' : 'Not Found' };
 			},
 			hooks: null,
 			manifest,
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index 3c971ce19..cbab3e1a5 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -432,7 +432,7 @@ export async function dev(vite, vite_config, svelte_config) {
 											'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'
 										);
 									}
-								}) ?? { message: event.routeId ? 'Internal Error' : 'Not Found' }
+								}) ?? { message: event.routeId != null ? 'Internal Error' : 'Not Found' }
 							);
 						},
 						hooks,
diff --git a/packages/kit/src/runtime/client/ambient.d.ts b/packages/kit/src/runtime/client/ambient.d.ts
index b9240974d..f4698f2d8 100644
--- a/packages/kit/src/runtime/client/ambient.d.ts
+++ b/packages/kit/src/runtime/client/ambient.d.ts
@@ -24,3 +24,7 @@ declare module '__GENERATED__/client-manifest.js' {
 
 	export const hooks: ClientHooks;
 }
+
+declare module '__GENERATED__/root.svelte' {
+	export { SvelteComponent as default } from 'svelte';
+}
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 23046c111..5066aed0c 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -1,5 +1,4 @@
 import { onMount, tick } from 'svelte';
-import { normalize_error } from '../../utils/error.js';
 import { make_trackable, decode_params, normalize_path } from '../../utils/url.js';
 import { find_anchor, get_base_uri, scroll_state } from './utils.js';
 import { lock_fetch, unlock_fetch, initial_fetch, subsequent_fetch } from './fetcher.js';
@@ -10,6 +9,7 @@ import { nodes, server_loads, dictionary, matchers, hooks } from '__GENERATED__/
 import { HttpError, Redirect } from '../control.js';
 import { stores } from './singletons.js';
 import { DATA_SUFFIX } from '../../constants.js';
+import { unwrap_promises } from '../../utils/promises.js';
 
 const SCROLL_KEY = 'sveltekit:scroll';
 const INDEX_KEY = 'sveltekit:index';
@@ -92,6 +92,8 @@ export function create_client({ target, base, trailing_slash }) {
 		url: null
 	};
 
+	/** this being true means we SSR'd */
+	let hydrated = false;
 	let started = false;
 	let autoscroll = true;
 	let updating = false;
@@ -211,27 +213,13 @@ export function create_client({ target, base, trailing_slash }) {
 		token = nav_token;
 		let navigation_result = intent && (await load_route(intent));
 
-		if (
-			!navigation_result &&
-			url.origin === location.origin &&
-			url.pathname === location.pathname
-		) {
-			// this could happen in SPA fallback mode if the user navigated to
-			// `/non-existent-page`. if we fall back to reloading the page, it
-			// will create an infinite loop. so whereas we normally handle
-			// unknown routes by going to the server, in this special case
-			// we render a client-side error page instead
-			navigation_result = await load_root_error_page({
-				status: 404,
-				error: new Error(`Not found: ${url.pathname}`),
-				url,
-				routeId: null
-			});
-		}
-
 		if (!navigation_result) {
-			await native_navigation(url);
-			return false; // unnecessary, but TypeScript prefers it this way
+			navigation_result = await server_fallback(
+				url,
+				null,
+				handle_error(new Error(`Not found: ${url.pathname}`), { url, params: {}, routeId: null }),
+				404
+			);
 		}
 
 		// if this is an internal navigation intent, use the normalized
@@ -245,7 +233,7 @@ export function create_client({ target, base, trailing_slash }) {
 			if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
 				navigation_result = await load_root_error_page({
 					status: 500,
-					error: new Error('Redirect loop'),
+					error: handle_error(new Error('Redirect loop'), { url, params: {}, routeId: null }),
 					url,
 					routeId: null
 				});
@@ -395,7 +383,7 @@ export function create_client({ target, base, trailing_slash }) {
 	 *   params: Record<string, string>;
 	 *   branch: Array<import('./types').BranchNode | undefined>;
 	 *   status: number;
-	 *   error: App.PageError | null;
+	 *   error: App.Error | null;
 	 *   route: import('types').CSRRoute | null;
 	 *   form?: Record<string, any> | null;
 	 * }} opts
@@ -621,6 +609,7 @@ export function create_client({ target, base, trailing_slash }) {
 			} else {
 				data = (await node.shared.load.call(null, load_input)) ?? null;
 			}
+			data = data ? await unwrap_promises(data) : null;
 		}
 
 		return {
@@ -722,7 +711,7 @@ export function create_client({ target, base, trailing_slash }) {
 			} catch (error) {
 				return load_root_error_page({
 					status: 500,
-					error: /** @type {Error} */ (error),
+					error: handle_error(error, { url, params, routeId: route.id }),
 					url,
 					routeId: route.id
 				});
@@ -799,7 +788,7 @@ export function create_client({ target, base, trailing_slash }) {
 					}
 
 					let status = 500;
-					/** @type {App.PageError} */
+					/** @type {App.Error} */
 					let error;
 
 					if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
@@ -827,8 +816,7 @@ export function create_client({ target, base, trailing_slash }) {
 					} else {
 						// if we get here, it's because the root `load` function failed,
 						// and we need to fall back to the server
-						await native_navigation(url);
-						return;
+						return await server_fallback(url, route.id, error, status);
 					}
 				}
 			} else {
@@ -882,7 +870,7 @@ export function create_client({ target, base, trailing_slash }) {
 	/**
 	 * @param {{
 	 *   status: number;
-	 *   error: HttpError | Error;
+	 *   error: App.Error;
 	 *   url: URL;
 	 *   routeId: string | null
 	 * }} opts
@@ -912,11 +900,11 @@ export function create_client({ target, base, trailing_slash }) {
 
 				server_data_node = server_data.nodes[0] ?? null;
 			} catch {
-				// at this point we have no choice but to fall back to the server
-				await native_navigation(url);
-
-				// @ts-expect-error
-				return;
+				// at this point we have no choice but to fall back to the server, if it wouldn't
+				// bring us right back here, turning this into an endless loop
+				if (url.origin !== location.origin || url.pathname !== location.pathname || hydrated) {
+					await native_navigation(url);
+				}
 			}
 		}
 
@@ -943,10 +931,7 @@ export function create_client({ target, base, trailing_slash }) {
 			params,
 			branch: [root_layout, root_error],
 			status,
-			error:
-				error instanceof HttpError
-					? error.body
-					: handle_error(error, { url, params, routeId: null }),
+			error,
 			route: null
 		});
 	}
@@ -1071,6 +1056,28 @@ export function create_client({ target, base, trailing_slash }) {
 		);
 	}
 
+	/**
+	 * Does a full page reload if it wouldn't result in an endless loop in the SPA case
+	 * @param {URL} url
+	 * @param {string | null} routeId
+	 * @param {App.Error} error
+	 * @param {number} status
+	 * @returns {Promise<import('./types').NavigationFinished>}
+	 */
+	async function server_fallback(url, routeId, error, status) {
+		if (url.origin === location.origin && url.pathname === location.pathname && !hydrated) {
+			// We would reload the same page we're currently on, which isn't hydrated,
+			// which means no SSR, which means we would end up in an endless loop
+			return await load_root_error_page({
+				status,
+				error,
+				url,
+				routeId
+			});
+		}
+		return await native_navigation(url);
+	}
+
 	/**
 	 * Loads `href` the old-fashioned way, with a full page reload.
 	 * Returns a `Promise` that never resolves (to prevent any
@@ -1413,6 +1420,8 @@ export function create_client({ target, base, trailing_slash }) {
 			data: server_data_nodes,
 			form
 		}) => {
+			hydrated = true;
+
 			const url = new URL(location.href);
 
 			/** @type {import('./types').NavigationFinished | undefined} */
@@ -1447,19 +1456,17 @@ export function create_client({ target, base, trailing_slash }) {
 					form,
 					route: routes.find((route) => route.id === routeId) ?? null
 				});
-			} catch (e) {
-				const error = normalize_error(e);
-
+			} catch (error) {
 				if (error instanceof Redirect) {
 					// this is a real edge case — `load` would need to return
 					// a redirect but only in the browser
-					await native_navigation(new URL(/** @type {Redirect} */ (e).location, location.href));
+					await native_navigation(new URL(error.location, location.href));
 					return;
 				}
 
 				result = await load_root_error_page({
 					status: error instanceof HttpError ? error.status : 500,
-					error,
+					error: handle_error(error, { url, params, routeId }),
 					url,
 					routeId
 				});
@@ -1504,12 +1511,15 @@ async function load_data(url, invalid) {
 /**
  * @param {unknown} error
  * @param {import('types').NavigationEvent} event
- * @returns {App.PageError}
+ * @returns {App.Error}
  */
 function handle_error(error, event) {
+	if (error instanceof HttpError) {
+		return error.body;
+	}
 	return (
 		hooks.handleError({ error, event }) ??
-		/** @type {any} */ ({ message: event.routeId ? 'Internal Error' : 'Not Found' })
+		/** @type {any} */ ({ message: event.routeId != null ? 'Internal Error' : 'Not Found' })
 	);
 }
 
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index 4e88d9132..3b441ec45 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -25,7 +25,7 @@ export interface Client {
 	// private API
 	_hydrate: (opts: {
 		status: number;
-		error: App.PageError;
+		error: App.Error;
 		node_ids: number[];
 		params: Record<string, string>;
 		routeId: string | null;
@@ -77,7 +77,7 @@ export interface DataNode {
 
 export interface NavigationState {
 	branch: Array<BranchNode | undefined>;
-	error: App.PageError | null;
+	error: App.Error | null;
 	params: Record<string, string>;
 	route: CSRRoute | null;
 	url: URL;
diff --git a/packages/kit/src/runtime/control.js b/packages/kit/src/runtime/control.js
index ba2e9db90..bd6c5d499 100644
--- a/packages/kit/src/runtime/control.js
+++ b/packages/kit/src/runtime/control.js
@@ -1,7 +1,7 @@
 export class HttpError {
 	/**
 	 * @param {number} status
-	 * @param {{message: string} extends App.PageError ? (App.PageError | string | undefined) : App.PageError} body
+	 * @param {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body
 	 */
 	constructor(status, body) {
 		this.status = status;
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index f80ed4417..d47eb0c13 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -32,7 +32,14 @@ export async function render_endpoint(event, mod, state) {
 	}
 
 	if (state.prerendering && !prerender) {
-		throw new Error(`${event.routeId} is not prerenderable`);
+		if (state.initiator) {
+			// if request came from a prerendered page, bail
+			throw new Error(`${event.routeId} is not prerenderable`);
+		} else {
+			// if request came direct from the crawler, signal that
+			// this route cannot be prerendered, but don't bail
+			return new Response(undefined, { status: 204 });
+		}
 	}
 
 	try {
diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js
index 21a753e50..4849956b7 100644
--- a/packages/kit/src/runtime/server/page/load_data.js
+++ b/packages/kit/src/runtime/server/page/load_data.js
@@ -1,5 +1,5 @@
 import { disable_search, make_trackable } from '../../../utils/url.js';
-
+import { unwrap_promises } from '../../../utils/promises.js';
 /**
  * Calls the user's `load` function.
  * @param {{
@@ -110,15 +110,3 @@ export async function load_data({ event, fetcher, node, parent, server_data_prom
 
 	return data ? unwrap_promises(data) : null;
 }
-
-/** @param {Record<string, any>} object */
-async function unwrap_promises(object) {
-	/** @type {Record<string, any>} */
-	const unwrapped = {};
-
-	for (const key in object) {
-		unwrapped[key] = await object[key];
-	}
-
-	return unwrapped;
-}
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 2bb13b92d..66fb873b7 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -23,7 +23,7 @@ const updated = {
  *   state: import('types').SSRState;
  *   page_config: { ssr: boolean; csr: boolean };
  *   status: number;
- *   error: App.PageError | null;
+ *   error: App.Error | null;
  *   event: import('types').RequestEvent;
  *   resolve_opts: import('types').RequiredResolveOptions;
  *   action_result?: import('types').ActionResult;
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index c137f9f4f..00bb1e4d9 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -154,7 +154,7 @@ export function handle_fatal_error(event, options, error) {
  * @param {import('types').RequestEvent} event
  * @param {import('types').SSROptions} options
  * @param {any} error
- * @returns {App.PageError}
+ * @returns {App.Error}
  */
 export function handle_error_and_jsonify(event, options, error) {
 	if (error instanceof HttpError) {
diff --git a/packages/kit/src/utils/promises.js b/packages/kit/src/utils/promises.js
new file mode 100644
index 000000000..ac2b53a60
--- /dev/null
+++ b/packages/kit/src/utils/promises.js
@@ -0,0 +1,17 @@
+/**
+ * Given an object, return a new object where all top level values are awaited
+ *
+ * @param {Record<string, any>} object
+ * @returns {Promise<Record<string, any>>}
+ */
+export async function unwrap_promises(object) {
+	for (const key in object) {
+		if (typeof object[key]?.then === 'function') {
+			return Object.fromEntries(
+				await Promise.all(Object.entries(object).map(async ([key, value]) => [key, await value]))
+			);
+		}
+	}
+
+	return object;
+}
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 026ce6c5e..2405fa7ac 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -40,6 +40,13 @@
  *
  */
 declare namespace App {
+	/**
+	 * Defines the common shape of expected and unexpected errors. Expected errors are thrown using the `error` function. Unexpected errors are handled by the `handleError` hooks which should return this shape.
+	 */
+	export interface Error {
+		message: string;
+	}
+
 	/**
 	 * The interface that defines `event.locals`, which can be accessed in [hooks](https://kit.svelte.dev/docs/hooks) (`handle`, and `handleError`), server-only `load` functions, and `+server.js` files.
 	 */
@@ -56,13 +63,6 @@ declare namespace App {
 	 * If your adapter provides [platform-specific context](https://kit.svelte.dev/docs/adapters#supported-environments-platform-specific-context) via `event.platform`, you can specify it here.
 	 */
 	export interface Platform {}
-
-	/**
-	 * Defines the common shape of expected and unexpected errors. Expected errors are thrown using the `error` function. Unexpected errors are handled by the `handleError` hooks which should return this shape.
-	 */
-	export interface PageError {
-		message: string;
-	}
 }
 
 /**
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index c48dba8ad..fcf925ca1 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -236,11 +236,11 @@ export interface Handle {
 }
 
 export interface HandleServerError {
-	(input: { error: unknown; event: RequestEvent }): void | App.PageError;
+	(input: { error: unknown; event: RequestEvent }): void | App.Error;
 }
 
 export interface HandleClientError {
-	(input: { error: unknown; event: NavigationEvent }): void | App.PageError;
+	(input: { error: unknown; event: NavigationEvent }): void | App.Error;
 }
 
 export interface HandleFetch {
@@ -300,7 +300,7 @@ export interface Page<Params extends Record<string, string> = Record<string, str
 	params: Params;
 	routeId: string | null;
 	status: number;
-	error: App.PageError | null;
+	error: App.Error | null;
 	data: App.PageData & Record<string, any>;
 }
 
@@ -415,13 +415,13 @@ export type ActionResult<
  * This object, if thrown during request handling, will cause SvelteKit to
  * return an error response without invoking `handleError`
  * @param status The HTTP status code
- * @param body An object that conforms to the App.PageError type. If a string is passed, it will be used as the message property.
+ * @param body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
  */
-export function error(status: number, body: App.PageError): HttpError;
+export function error(status: number, body: App.Error): HttpError;
 export function error(
 	status: number,
-	// this overload ensures you can omit the argument or pass in a string if App.PageError is of type { message: string }
-	body?: { message: string } extends App.PageError ? App.PageError | string | undefined : never
+	// this overload ensures you can omit the argument or pass in a string if App.Error is of type { message: string }
+	body?: { message: string } extends App.Error ? App.Error | string | undefined : never
 ): HttpError;
 
 /**
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 29f84240d..3f7ff7d76 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -226,7 +226,7 @@ export interface ServerDataSkippedNode {
  */
 export interface ServerErrorNode {
 	type: 'error';
-	error: App.PageError;
+	error: App.Error;
 	/**
 	 * Only set for HttpErrors
 	 */
@@ -288,7 +288,7 @@ export interface SSROptions {
 		check_origin: boolean;
 	};
 	dev: boolean;
-	handle_error(error: Error & { frame?: string }, event: RequestEvent): App.PageError;
+	handle_error(error: Error & { frame?: string }, event: RequestEvent): App.Error;
 	hooks: ServerHooks;
 	manifest: SSRManifest;
 	paths: {


Test patch:
diff --git a/packages/kit/test/prerendering/options/src/hooks.server.js b/packages/kit/test/prerendering/options/src/hooks.server.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/packages/kit/test/prerendering/options/src/routes/+layout.svelte b/packages/kit/test/prerendering/options/src/routes/+layout.svelte
index 4fa864ce7..cbf29e45a 100644
--- a/packages/kit/test/prerendering/options/src/routes/+layout.svelte
+++ b/packages/kit/test/prerendering/options/src/routes/+layout.svelte
@@ -1,1 +1,5 @@
+<svelte:head>
+	<link rel="alternate" href="/path-base/rss.xml" />
+</svelte:head>
+
 <slot />
diff --git a/packages/kit/test/prerendering/options/src/routes/rss.xml/+server.js b/packages/kit/test/prerendering/options/src/routes/rss.xml/+server.js
new file mode 100644
index 000000000..f872bacf4
--- /dev/null
+++ b/packages/kit/test/prerendering/options/src/routes/rss.xml/+server.js
@@ -0,0 +1,3 @@
+export function GET() {
+	return new Response('dynamic');
+}
diff --git a/packages/kit/test/prerendering/options/test/test.js b/packages/kit/test/prerendering/options/test/test.js
index 7f104b4db..910fb2a5e 100644
--- a/packages/kit/test/prerendering/options/test/test.js
+++ b/packages/kit/test/prerendering/options/test/test.js
@@ -37,4 +37,8 @@ test('populates fallback 200.html file', () => {
 	assert.ok(content !== '');
 });
 
+test('does not prerender linked +server.js route', () => {
+	assert.ok(!fs.existsSync(`${build}/rss.xml`));
+});
+
 test.run();