Repository: sveltejs/kit
Pull request #6901 (2022-09-19 19:49:55)

fix: (issue-6608) Add origin header to server-side fetch
Add the `Origin` header to server-side fetch. 
The problem has been introduced with #6550, where browser CORS behaviour was simulated for the server-side requests. For CORS requests to work properly, the `Origin` header is mandatory.

Fixes: #6608 

### 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. All changesets should be `patch` until SvelteKit 1.0


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: b5bc688a278c70a873a208f392bd1502fd817b70

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/bummzack/kit/new/fix/issue-6608?filename=.changeset/slimy-bats-lick.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A---%0A%0Afix%3A%20(issue-6608)%20Add%20origin%20header%20to%20server-side%20fetch%0A)



Patch:
diff --git a/.changeset/afraid-gifts-act.md b/.changeset/afraid-gifts-act.md
new file mode 100644
index 000000000..7f4a0c421
--- /dev/null
+++ b/.changeset/afraid-gifts-act.md
@@ -0,0 +1,6 @@
+---
+'@sveltejs/adapter-cloudflare': patch
+'@sveltejs/kit': patch
+---
+
+Support Cloudflare Pages `_routes.json` specification
diff --git a/.changeset/cool-beans-only.md b/.changeset/cool-beans-only.md
new file mode 100644
index 000000000..755343640
--- /dev/null
+++ b/.changeset/cool-beans-only.md
@@ -0,0 +1,5 @@
+---
+"@sveltejs/kit": patch
+---
+
+fix `write_types` on windows using posixify()
diff --git a/.changeset/cuddly-dragons-punch.md b/.changeset/cuddly-dragons-punch.md
new file mode 100644
index 000000000..1491e81fb
--- /dev/null
+++ b/.changeset/cuddly-dragons-punch.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Fix fetch type
diff --git a/.changeset/cyan-years-live.md b/.changeset/cyan-years-live.md
new file mode 100644
index 000000000..0ed5696bb
--- /dev/null
+++ b/.changeset/cyan-years-live.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Prevent caching of `__data.js` files
diff --git a/.changeset/dirty-candles-peel.md b/.changeset/dirty-candles-peel.md
new file mode 100644
index 000000000..5ea2f31ae
--- /dev/null
+++ b/.changeset/dirty-candles-peel.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] allow missing routes folder
diff --git a/.changeset/dry-snails-remain.md b/.changeset/dry-snails-remain.md
new file mode 100644
index 000000000..448501302
--- /dev/null
+++ b/.changeset/dry-snails-remain.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] add missing depends function to ServerLoadEvent type
diff --git a/.changeset/dull-cows-sneeze.md b/.changeset/dull-cows-sneeze.md
new file mode 100644
index 000000000..fd084fe7d
--- /dev/null
+++ b/.changeset/dull-cows-sneeze.md
@@ -0,0 +1,6 @@
+---
+'@sveltejs/adapter-cloudflare': patch
+'@sveltejs/adapter-cloudflare-workers': patch
+---
+
+Include ambient.d.ts files in adapter packages.
diff --git a/.changeset/early-olives-burn.md b/.changeset/early-olives-burn.md
new file mode 100644
index 000000000..ae81419bf
--- /dev/null
+++ b/.changeset/early-olives-burn.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-node': patch
+---
+
+Add missing Rollup dependency
diff --git a/.changeset/fifty-suns-hope.md b/.changeset/fifty-suns-hope.md
new file mode 100644
index 000000000..c261c2b40
--- /dev/null
+++ b/.changeset/fifty-suns-hope.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] update page status when applying action
diff --git a/.changeset/five-tools-arrive.md b/.changeset/five-tools-arrive.md
new file mode 100644
index 000000000..139d51a0e
--- /dev/null
+++ b/.changeset/five-tools-arrive.md
@@ -0,0 +1,7 @@
+---
+"@sveltejs/adapter-node": patch
+"@sveltejs/adapter-vercel": patch
+"@sveltejs/kit": patch
+---
+
+Redact error message if `getRequest` fails
diff --git a/.changeset/flat-owls-care.md b/.changeset/flat-owls-care.md
new file mode 100644
index 000000000..484be96f9
--- /dev/null
+++ b/.changeset/flat-owls-care.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Add cookies.serialize method
diff --git a/.changeset/great-tables-learn.md b/.changeset/great-tables-learn.md
new file mode 100644
index 000000000..8d6d60440
--- /dev/null
+++ b/.changeset/great-tables-learn.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Run `svelte-kit sync` in all workspace directories during postinstall
diff --git a/.changeset/hot-steaks-hang.md b/.changeset/hot-steaks-hang.md
new file mode 100644
index 000000000..e37477276
--- /dev/null
+++ b/.changeset/hot-steaks-hang.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Add origin header for non-GET and external requests made with server-side fetch
diff --git a/.changeset/mean-chefs-swim.md b/.changeset/mean-chefs-swim.md
new file mode 100644
index 000000000..f37560970
--- /dev/null
+++ b/.changeset/mean-chefs-swim.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Add csp wasm-unsafe-eval keyword
diff --git a/.changeset/odd-beds-smile.md b/.changeset/odd-beds-smile.md
new file mode 100644
index 000000000..63aa75272
--- /dev/null
+++ b/.changeset/odd-beds-smile.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-node': patch
+---
+
+Bundle with rollup instead of esbuild
diff --git a/.changeset/orange-cups-itch.md b/.changeset/orange-cups-itch.md
new file mode 100644
index 000000000..7a0b57312
--- /dev/null
+++ b/.changeset/orange-cups-itch.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Register service worker with relative URL
diff --git a/.changeset/orange-rocks-return.md b/.changeset/orange-rocks-return.md
new file mode 100644
index 000000000..74075106f
--- /dev/null
+++ b/.changeset/orange-rocks-return.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-cloudflare': patch
+---
+
+Ensure all excluded paths in `_routes.json` start with /
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 2bb940b06..7f4c410cc 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -45,6 +45,7 @@
     "afraid-eels-tease",
     "afraid-flowers-train",
     "afraid-games-jog",
+    "afraid-gifts-act",
     "afraid-kings-mix",
     "afraid-llamas-matter",
     "afraid-oranges-doubt",
@@ -179,6 +180,7 @@
     "cold-shirts-lick",
     "cold-tables-shake",
     "cool-bananas-share",
+    "cool-beans-only",
     "cool-birds-attack",
     "cool-crabs-listen",
     "cool-drinks-press",
@@ -187,6 +189,7 @@
     "cool-spoons-dress",
     "cool-swans-sleep",
     "cuddly-ads-kick",
+    "cuddly-dragons-punch",
     "cuddly-files-decide",
     "cuddly-knives-learn",
     "cuddly-months-sip",
@@ -215,13 +218,17 @@
     "cyan-roses-act",
     "cyan-schools-kiss",
     "cyan-seals-trade",
+    "cyan-years-live",
     "dirty-bikes-join",
+    "dirty-candles-peel",
     "dirty-ligers-protect",
     "dry-baboons-turn",
     "dry-cars-dance",
     "dry-jokes-explain",
+    "dry-snails-remain",
     "dry-yaks-clean",
     "dull-cheetahs-clean",
+    "dull-cows-sneeze",
     "dull-ducks-report",
     "dull-emus-tap",
     "dull-lobsters-act",
@@ -235,6 +242,7 @@
     "early-eagles-boil",
     "early-lemons-collect",
     "early-news-taste",
+    "early-olives-burn",
     "early-pants-unite",
     "early-snakes-peel",
     "early-wasps-obey",
@@ -305,6 +313,7 @@
     "few-walls-obey",
     "fifty-cougars-agree",
     "fifty-foxes-tan",
+    "fifty-suns-hope",
     "fifty-taxis-tease",
     "fifty-turtles-joke",
     "five-bags-prove",
@@ -317,11 +326,13 @@
     "five-poems-pretend",
     "five-seas-end",
     "five-shirts-allow",
+    "five-tools-arrive",
     "five-turtles-rhyme",
     "flat-cameras-build",
     "flat-ducks-impress",
     "flat-eels-hear",
     "flat-insects-worry",
+    "flat-owls-care",
     "flat-parrots-juggle",
     "flat-pears-compare",
     "flat-pillows-think",
@@ -418,6 +429,7 @@
     "great-lies-agree",
     "great-queens-rule",
     "great-seahorses-fry",
+    "great-tables-learn",
     "green-garlics-warn",
     "green-kiwis-roll",
     "green-masks-confess",
@@ -613,6 +625,7 @@
     "many-pugs-work",
     "many-suns-push",
     "mean-bananas-live",
+    "mean-chefs-swim",
     "mean-crews-unite",
     "mean-mangos-marry",
     "mean-pets-confess",
@@ -723,6 +736,7 @@
     "ninety-tables-switch",
     "ninety-vans-talk",
     "ninety-walls-heal",
+    "odd-beds-smile",
     "odd-bottles-drop",
     "odd-coats-shake",
     "odd-coins-lie",
@@ -754,6 +768,8 @@
     "olive-socks-thank",
     "olive-vans-talk",
     "orange-boats-love",
+    "orange-cups-itch",
+    "orange-rocks-return",
     "orange-ties-tell",
     "orange-wolves-fly",
     "perfect-cycles-smell",
@@ -962,6 +978,7 @@
     "short-emus-juggle",
     "short-guests-sort",
     "short-lamps-sing",
+    "short-spies-relax",
     "shy-bats-move",
     "shy-jeans-fly",
     "shy-mails-share",
@@ -969,6 +986,8 @@
     "shy-toes-punch",
     "silent-crews-change",
     "silent-cycles-reply",
+    "silent-jeans-vanish",
+    "silent-wasps-sniff",
     "silly-ants-battle",
     "silly-grapes-cover",
     "silly-jokes-hug",
@@ -981,6 +1000,7 @@
     "silver-hounds-clean",
     "silver-lemons-fail",
     "silver-needles-judge",
+    "silver-pugs-worry",
     "silver-rice-flash",
     "silver-scissors-peel",
     "silver-toes-cheer",
@@ -1014,6 +1034,7 @@
     "smart-apes-drum",
     "smart-bears-leave",
     "smart-deers-approve",
+    "smart-forks-double",
     "smart-humans-eat",
     "smart-mugs-chew",
     "smart-nails-work",
@@ -1022,6 +1043,7 @@
     "smart-peas-compare",
     "smart-planets-beam",
     "smooth-bags-sell",
+    "smooth-bears-unite",
     "smooth-cars-build",
     "smooth-cars-reflect",
     "smooth-comics-mate",
@@ -1088,6 +1110,7 @@
     "strange-kiwis-tell",
     "strange-poems-design",
     "strong-apples-walk",
+    "strong-baboons-travel",
     "strong-carrots-work",
     "strong-cats-wonder",
     "strong-cooks-run",
@@ -1340,6 +1363,7 @@
     "witty-maps-pull",
     "witty-meals-tie",
     "witty-news-dress",
+    "witty-shoes-know",
     "yellow-apricots-stare",
     "yellow-coins-wash",
     "yellow-eagles-carry",
@@ -1363,6 +1387,7 @@
     "young-pumpkins-approve",
     "young-scissors-collect",
     "young-students-chew",
+    "young-students-relate",
     "young-swans-burn",
     "young-ways-fail"
   ]
diff --git a/.changeset/short-spies-relax.md b/.changeset/short-spies-relax.md
new file mode 100644
index 000000000..b358986e8
--- /dev/null
+++ b/.changeset/short-spies-relax.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Always apply cookies, not just for matched routes
diff --git a/.changeset/silent-jeans-vanish.md b/.changeset/silent-jeans-vanish.md
new file mode 100644
index 000000000..44b09d9c1
--- /dev/null
+++ b/.changeset/silent-jeans-vanish.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] symlink routes
diff --git a/.changeset/silent-wasps-sniff.md b/.changeset/silent-wasps-sniff.md
new file mode 100644
index 000000000..f6ec7b038
--- /dev/null
+++ b/.changeset/silent-wasps-sniff.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] tighten up navigation and invalidation logic
diff --git a/.changeset/silver-pugs-worry.md b/.changeset/silver-pugs-worry.md
new file mode 100644
index 000000000..27e98e25a
--- /dev/null
+++ b/.changeset/silver-pugs-worry.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] tighten up handling and documentation around 404
diff --git a/.changeset/smart-forks-double.md b/.changeset/smart-forks-double.md
new file mode 100644
index 000000000..e98f3bbab
--- /dev/null
+++ b/.changeset/smart-forks-double.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/package': patch
+---
+
+fix check for undefined on application/ strip
diff --git a/.changeset/smooth-bears-unite.md b/.changeset/smooth-bears-unite.md
new file mode 100644
index 000000000..8659e746f
--- /dev/null
+++ b/.changeset/smooth-bears-unite.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] apply redirect/error logic for `use:enhance`
diff --git a/.changeset/strong-baboons-travel.md b/.changeset/strong-baboons-travel.md
new file mode 100644
index 000000000..b25d46176
--- /dev/null
+++ b/.changeset/strong-baboons-travel.md
@@ -0,0 +1,5 @@
+---
+"@sveltejs/kit": patch
+---
+
+Respond with 413 if request body is too large
diff --git a/.changeset/witty-shoes-know.md b/.changeset/witty-shoes-know.md
new file mode 100644
index 000000000..045cf5139
--- /dev/null
+++ b/.changeset/witty-shoes-know.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Only forward set-cookie headers for internal fetches
diff --git a/.changeset/young-students-relate.md b/.changeset/young-students-relate.md
new file mode 100644
index 000000000..06b05a32b
--- /dev/null
+++ b/.changeset/young-students-relate.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Make url property getters non-enumerable
diff --git a/.gitignore b/.gitignore
index 77c2b8810..f53c51190 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,4 @@ vite.config.js.timestamp-*
 .turbo
 .vercel
 .test-tmp
-
+symlink-from
diff --git a/documentation/docs/04-advanced-routing.md b/documentation/docs/04-advanced-routing.md
index 05c1c813f..2f9d67abe 100644
--- a/documentation/docs/04-advanced-routing.md
+++ b/documentation/docs/04-advanced-routing.md
@@ -22,7 +22,11 @@ If the number of route segments is unknown, you can use rest syntax — for exam
 }
 ```
 
-This also allows you to render custom 404s. Given these routes...
+> `src/routes/a/[...rest]/z/+page.svelte` will match `/a/z` (i.e. there's no parameter at all) as well as `/a/b/z` and `/a/b/c/z` and so on. Make sure you check that the value of the rest parameter is valid, for example using a [matcher](#matching).
+
+#### 404 pages
+
+Rest parameters also allow you to render custom 404s. Given these routes...
 
 ```
 src/routes/
@@ -47,7 +51,17 @@ src/routes/
 └ +error.svelte
 ```
 
-> `src/routes/a/[...rest]/z/+page.svelte` will match `/a/z` (i.e. there's no parameter at all) as well as `/a/b/z` and `/a/b/c/z` and so on. Make sure you check that the value of the rest parameter is valid, for example using a [matcher](#matching).
+```js
+/// file: src/routes/marx-brothers/[...path]/+page.js
+import { error } from '@sveltejs/kit';
+
+/** @type {import('./$types').PageLoad} */
+export function load(event) {
+	throw error(404, 'Not Found');
+}
+```
+
+> If you don't handle 404 cases, they will appear in [`handleError`](/docs/hooks#shared-hooks-handleerror)
 
 ### Matching
 
diff --git a/documentation/docs/07-hooks.md b/documentation/docs/07-hooks.md
index 1b1dd3adb..8ede315c3 100644
--- a/documentation/docs/07-hooks.md
+++ b/documentation/docs/07-hooks.md
@@ -136,7 +136,7 @@ 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, which defaults to `{ message: 'Internal Error' }`, becomes the value of `$page.error`. 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.PageError` 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:
 
diff --git a/package.json b/package.json
index 817041bcc..6109081fc 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
 		"turbo": "^1.3.4",
 		"typescript": "^4.8.2"
 	},
-	"packageManager": "pnpm@7.12.0",
+	"packageManager": "pnpm@7.12.1",
 	"engines": {
 		"pnpm": "^7.0.0"
 	},
diff --git a/packages/adapter-auto/CHANGELOG.md b/packages/adapter-auto/CHANGELOG.md
index 8343aa61a..ca546206b 100644
--- a/packages/adapter-auto/CHANGELOG.md
+++ b/packages/adapter-auto/CHANGELOG.md
@@ -1,5 +1,36 @@
 # @sveltejs/adapter-auto
 
+## 1.0.0-next.80
+
+### Patch Changes
+
+- Updated dependencies [[`d0e63d44`](https://github.com/sveltejs/kit/commit/d0e63d445e19e2df7a5e7d35f42b2ba1ae5e5745)]:
+  - @sveltejs/adapter-cloudflare@1.0.0-next.38
+
+## 1.0.0-next.79
+
+### Patch Changes
+
+- Updated dependencies [[`ae8225d4`](https://github.com/sveltejs/kit/commit/ae8225d4e547c8bc07d82e516dd49315eddb56c3)]:
+  - @sveltejs/adapter-cloudflare@1.0.0-next.37
+  - @sveltejs/adapter-netlify@1.0.0-next.78
+  - @sveltejs/adapter-vercel@1.0.0-next.77
+
+## 1.0.0-next.78
+
+### Patch Changes
+
+- Updated dependencies [[`737160d8`](https://github.com/sveltejs/kit/commit/737160d88cd96323eeac317ed08b4f0d6d83c482)]:
+  - @sveltejs/adapter-vercel@1.0.0-next.77
+  - @sveltejs/adapter-netlify@1.0.0-next.78
+
+## 1.0.0-next.77
+
+### Patch Changes
+
+- Updated dependencies [[`e40acb56`](https://github.com/sveltejs/kit/commit/e40acb561e32a2ee5bcb6d289ebd1284d7af247b)]:
+  - @sveltejs/adapter-cloudflare@1.0.0-next.36
+
 ## 1.0.0-next.76
 
 ### Patch Changes
diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json
index 10e404e4a..859537000 100644
--- a/packages/adapter-auto/package.json
+++ b/packages/adapter-auto/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-auto",
-	"version": "1.0.0-next.76",
+	"version": "1.0.0-next.80",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-cloudflare-workers/CHANGELOG.md b/packages/adapter-cloudflare-workers/CHANGELOG.md
index b2322760e..2927372a0 100644
--- a/packages/adapter-cloudflare-workers/CHANGELOG.md
+++ b/packages/adapter-cloudflare-workers/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-cloudflare-workers
 
+## 1.0.0-next.57
+
+### Patch Changes
+
+- Include ambient.d.ts files in adapter packages. ([#6917](https://github.com/sveltejs/kit/pull/6917))
+
 ## 1.0.0-next.56
 
 ### Patch Changes
diff --git a/packages/adapter-cloudflare-workers/package.json b/packages/adapter-cloudflare-workers/package.json
index f856b8e32..2504210b5 100644
--- a/packages/adapter-cloudflare-workers/package.json
+++ b/packages/adapter-cloudflare-workers/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-cloudflare-workers",
-	"version": "1.0.0-next.56",
+	"version": "1.0.0-next.57",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -19,6 +19,7 @@
 	"main": "index.js",
 	"types": "index.d.ts",
 	"files": [
+		"ambient.d.ts",
 		"files",
 		"index.d.ts"
 	],
diff --git a/packages/adapter-cloudflare/CHANGELOG.md b/packages/adapter-cloudflare/CHANGELOG.md
index 456216ec9..fc1d02e4c 100644
--- a/packages/adapter-cloudflare/CHANGELOG.md
+++ b/packages/adapter-cloudflare/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @sveltejs/adapter-cloudflare
 
+## 1.0.0-next.38
+
+### Patch Changes
+
+- Ensure all excluded paths in `_routes.json` start with / ([#6952](https://github.com/sveltejs/kit/pull/6952))
+
+## 1.0.0-next.37
+
+### Patch Changes
+
+- Support Cloudflare Pages `_routes.json` specification ([#6530](https://github.com/sveltejs/kit/pull/6530))
+
+## 1.0.0-next.36
+
+### Patch Changes
+
+- Include ambient.d.ts files in adapter packages. ([#6917](https://github.com/sveltejs/kit/pull/6917))
+
 ## 1.0.0-next.35
 
 ### Patch Changes
diff --git a/packages/adapter-cloudflare/index.d.ts b/packages/adapter-cloudflare/index.d.ts
index ed89676da..1e47e1627 100644
--- a/packages/adapter-cloudflare/index.d.ts
+++ b/packages/adapter-cloudflare/index.d.ts
@@ -2,3 +2,10 @@ import { Adapter } from '@sveltejs/kit';
 import './ambient.js';
 
 export default function plugin(): Adapter;
+
+export interface RoutesJSONSpec {
+	version: 1;
+	description: string;
+	include: string[];
+	exclude: string[];
+}
diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js
index bfb9d968a..0b6eb5c41 100644
--- a/packages/adapter-cloudflare/index.js
+++ b/packages/adapter-cloudflare/index.js
@@ -23,18 +23,28 @@ export default function () {
 			builder.rimraf(tmp);
 			builder.mkdirp(tmp);
 
-			builder.writeClient(dest);
+			const written_files = builder.writeClient(dest);
 			builder.writePrerendered(dest);
 
 			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\nexport const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
+				`export const manifest = ${builder.generateManifest({ relativePath })};\n\n` +
+					`export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
+			);
+
+			writeFileSync(
+				`${dest}/_routes.json`,
+				JSON.stringify(get_routes_json(builder.config.kit.appDir, written_files))
 			);
 
+			writeFileSync(`${dest}/_headers`, generate_headers(builder.config.kit.appDir));
+
 			builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, {
 				replace: {
 					SERVER: `${relativePath}/index.js`,
@@ -55,3 +65,41 @@ export default function () {
 		}
 	};
 }
+
+/**
+ * @param {string} app_dir
+ * @param {string[]} assets
+ * @returns {import('.').RoutesJSONSpec}
+ */
+function get_routes_json(app_dir, assets) {
+	return {
+		version: 1,
+		description: 'Generated by @sveltejs/adapter-cloudflare',
+		include: ['/*'],
+		exclude: [
+			`/${app_dir}/immutable/*`,
+			...assets
+				// We're being conservative by not excluding all assets in
+				// /static just yet. If there are any upstream auth rules to
+				// protect certain things (e.g. a PDF that requires auth),
+				// then we wouldn't want to prevent those requests from going
+				// to the user functions worker.
+				// We do want to show an example of a _routes.json that
+				// excludes more than just /_app/immutable/*, and favicons
+				// are a reasonable choice
+				.filter((file) => file.startsWith('favicon'))
+				.map((file) => `/${file}`)
+		]
+	};
+}
+
+/** @param {string} app_dir */
+function generate_headers(app_dir) {
+	return `
+# === START AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
+/${app_dir}/immutable/*
+	Cache-Control: public, immutable, max-age=31536000
+	X-Robots-Tag: noindex
+# === END AUTOGENERATED SVELTE IMMUTABLE HEADERS ===
+	`.trim();
+}
diff --git a/packages/adapter-cloudflare/package.json b/packages/adapter-cloudflare/package.json
index a27f5eaac..5c180ae58 100644
--- a/packages/adapter-cloudflare/package.json
+++ b/packages/adapter-cloudflare/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-cloudflare",
-	"version": "1.0.0-next.35",
+	"version": "1.0.0-next.38",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -20,7 +20,8 @@
 	"files": [
 		"files",
 		"index.js",
-		"index.d.ts"
+		"index.d.ts",
+		"ambient.d.ts"
 	],
 	"main": "index.js",
 	"scripts": {
diff --git a/packages/adapter-node/CHANGELOG.md b/packages/adapter-node/CHANGELOG.md
index 43ac0fa07..4871493d2 100644
--- a/packages/adapter-node/CHANGELOG.md
+++ b/packages/adapter-node/CHANGELOG.md
@@ -1,5 +1,23 @@
 # @sveltejs/adapter-node
 
+## 1.0.0-next.95
+
+### Patch Changes
+
+- Redact error message if `getRequest` fails ([#6936](https://github.com/sveltejs/kit/pull/6936))
+
+## 1.0.0-next.94
+
+### Patch Changes
+
+- Add missing Rollup dependency ([#6927](https://github.com/sveltejs/kit/pull/6927))
+
+## 1.0.0-next.93
+
+### Patch Changes
+
+- Bundle with rollup instead of esbuild ([#6896](https://github.com/sveltejs/kit/pull/6896))
+
 ## 1.0.0-next.92
 
 ### Patch Changes
diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js
index cc3d0da78..17bb5310c 100644
--- a/packages/adapter-node/index.js
+++ b/packages/adapter-node/index.js
@@ -1,6 +1,9 @@
 import { readFileSync, writeFileSync } from 'fs';
 import { fileURLToPath } from 'url';
-import * as esbuild from 'esbuild';
+import { rollup } from 'rollup';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+import json from '@rollup/plugin-json';
 
 const files = fileURLToPath(new URL('./files', import.meta.url).href);
 
@@ -49,16 +52,23 @@ export default function (opts = {}) {
 
 			const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
 
-			await esbuild.build({
-				platform: 'node',
-				sourcemap: 'linked',
-				target: 'es2022',
-				entryPoints: [`${tmp}/index.js`, `${tmp}/manifest.js`],
-				outdir: `${out}/server`,
-				splitting: true,
+			// we bundle the Vite output so that deployments only need
+			// their production dependencies. Anything in devDependencies
+			// will get included in the bundled code
+			const bundle = await rollup({
+				input: {
+					index: `${tmp}/index.js`,
+					manifest: `${tmp}/manifest.js`
+				},
+				external: [...Object.keys(pkg.dependencies || {})],
+				plugins: [nodeResolve(), commonjs(), json()]
+			});
+
+			await bundle.write({
+				dir: `${out}/server`,
 				format: 'esm',
-				bundle: true,
-				external: [...Object.keys(pkg.dependencies || {})]
+				sourcemap: true,
+				chunkFileNames: `chunks/[name]-[hash].js`
 			});
 
 			builder.copy(files, out, {
diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json
index c5d40c7fb..c10387b3f 100644
--- a/packages/adapter-node/package.json
+++ b/packages/adapter-node/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-node",
-	"version": "1.0.0-next.92",
+	"version": "1.0.0-next.95",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -32,19 +32,20 @@
 		"prepublishOnly": "npm run build"
 	},
 	"devDependencies": {
-		"@rollup/plugin-json": "^4.1.0",
 		"@sveltejs/kit": "workspace:*",
 		"@types/node": "^16.11.36",
 		"c8": "^7.11.3",
 		"node-fetch": "^3.2.4",
 		"polka": "^1.0.0-next.22",
 		"rimraf": "^3.0.2",
-		"rollup": "^2.78.1",
 		"sirv": "^2.0.2",
 		"typescript": "^4.8.2",
 		"uvu": "^0.5.3"
 	},
 	"dependencies": {
-		"esbuild": "^0.15.7"
+		"@rollup/plugin-commonjs": "^22.0.1",
+		"@rollup/plugin-json": "^4.1.0",
+		"@rollup/plugin-node-resolve": "^14.1.0",
+		"rollup": "^2.78.1"
 	}
 }
diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js
index 94f331f57..7177e836b 100644
--- a/packages/adapter-node/src/handler.js
+++ b/packages/adapter-node/src/handler.js
@@ -56,7 +56,7 @@ const ssr = async (req, res) => {
 		});
 	} catch (err) {
 		res.statusCode = err.status || 400;
-		res.end(err.reason || 'Invalid request body');
+		res.end('Invalid request body');
 		return;
 	}
 
diff --git a/packages/adapter-vercel/CHANGELOG.md b/packages/adapter-vercel/CHANGELOG.md
index 8f89e664f..8d4d31b63 100644
--- a/packages/adapter-vercel/CHANGELOG.md
+++ b/packages/adapter-vercel/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-vercel
 
+## 1.0.0-next.77
+
+### Patch Changes
+
+- Redact error message if `getRequest` fails ([#6936](https://github.com/sveltejs/kit/pull/6936))
+
 ## 1.0.0-next.76
 
 ### Patch Changes
diff --git a/packages/adapter-vercel/files/serverless.js b/packages/adapter-vercel/files/serverless.js
index 36fbae4c7..25db646be 100644
--- a/packages/adapter-vercel/files/serverless.js
+++ b/packages/adapter-vercel/files/serverless.js
@@ -23,7 +23,7 @@ export default async (req, res) => {
 		request = await getRequest({ base: `https://${req.headers.host}`, request: req });
 	} catch (err) {
 		res.statusCode = err.status || 400;
-		return res.end(err.reason || 'Invalid request body');
+		return res.end('Invalid request body');
 	}
 
 	setResponse(
diff --git a/packages/adapter-vercel/package.json b/packages/adapter-vercel/package.json
index 6cd9ec393..3a81edde3 100644
--- a/packages/adapter-vercel/package.json
+++ b/packages/adapter-vercel/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-vercel",
-	"version": "1.0.0-next.76",
+	"version": "1.0.0-next.77",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 583f6325b..b9f2667b9 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,75 @@
 # @sveltejs/kit
 
+## 1.0.0-next.496
+
+### Patch Changes
+
+- [fix] update page status when applying action ([#6942](https://github.com/sveltejs/kit/pull/6942))
+
+* [fix] apply redirect/error logic for `use:enhance` ([#6828](https://github.com/sveltejs/kit/pull/6828))
+
+## 1.0.0-next.495
+
+### Patch Changes
+
+- Run `svelte-kit sync` in all workspace directories during postinstall ([#6949](https://github.com/sveltejs/kit/pull/6949))
+
+## 1.0.0-next.494
+
+### Patch Changes
+
+- Fix fetch type ([#6955](https://github.com/sveltejs/kit/pull/6955))
+
+* Add csp wasm-unsafe-eval keyword ([#6958](https://github.com/sveltejs/kit/pull/6958))
+
+- Register service worker with relative URL ([#6954](https://github.com/sveltejs/kit/pull/6954))
+
+## 1.0.0-next.493
+
+### Patch Changes
+
+- [fix] allow missing routes folder ([#6944](https://github.com/sveltejs/kit/pull/6944))
+
+* Only forward set-cookie headers for internal fetches ([#6923](https://github.com/sveltejs/kit/pull/6923))
+
+## 1.0.0-next.492
+
+### Patch Changes
+
+- [fix] add missing depends function to ServerLoadEvent type ([#6935](https://github.com/sveltejs/kit/pull/6935))
+
+* Redact error message if `getRequest` fails ([#6936](https://github.com/sveltejs/kit/pull/6936))
+
+- Add cookies.serialize method ([#6925](https://github.com/sveltejs/kit/pull/6925))
+
+* Always apply cookies, not just for matched routes ([#6925](https://github.com/sveltejs/kit/pull/6925))
+
+- [fix] tighten up navigation and invalidation logic ([#6924](https://github.com/sveltejs/kit/pull/6924))
+
+* Respond with 413 if request body is too large ([#6936](https://github.com/sveltejs/kit/pull/6936))
+
+## 1.0.0-next.491
+
+### Patch Changes
+
+- [fix] symlink routes ([#6796](https://github.com/sveltejs/kit/pull/6796))
+
+## 1.0.0-next.490
+
+### Patch Changes
+
+- fix `write_types` on windows using posixify() ([#6913](https://github.com/sveltejs/kit/pull/6913))
+
+* Prevent caching of `__data.js` files ([#6904](https://github.com/sveltejs/kit/pull/6904))
+
+- Make url property getters non-enumerable ([#6909](https://github.com/sveltejs/kit/pull/6909))
+
+## 1.0.0-next.489
+
+### Patch Changes
+
+- [fix] tighten up handling and documentation around 404 ([#6897](https://github.com/sveltejs/kit/pull/6897))
+
 ## 1.0.0-next.488
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index bcd67f0d0..a2dd044a7 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.488",
+	"version": "1.0.0-next.496",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
@@ -54,6 +54,7 @@
 		"!src/core/**/test",
 		"types",
 		"svelte-kit.js",
+		"postinstall.js",
 		"scripts/special-types"
 	],
 	"scripts": {
@@ -67,7 +68,7 @@
 		"test:integration": "pnpm run -r --workspace-concurrency 1 --filter=\"./test/**\" test",
 		"test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\"",
 		"types": "node scripts/extract-types.js",
-		"postinstall": "node svelte-kit.js sync"
+		"postinstall": "node postinstall.js"
 	},
 	"exports": {
 		"./package.json": "./package.json",
diff --git a/packages/kit/postinstall.js b/packages/kit/postinstall.js
new file mode 100644
index 000000000..980337e5b
--- /dev/null
+++ b/packages/kit/postinstall.js
@@ -0,0 +1,37 @@
+import fs from 'fs';
+import path from 'path';
+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 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)));
+	}
+} else {
+	directories.push(cwd);
+}
+
+for (const cwd of directories) {
+	process.chdir(cwd);
+
+	if (!fs.existsSync('package.json')) continue;
+	if (!fs.existsSync('svelte.config.js')) continue;
+
+	const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
+	if (!pkg.dependencies?.['@sveltejs/kit'] && !pkg.devDependencies?.['@sveltejs/kit']) continue;
+
+	try {
+		const config = await load_config();
+		await sync.all(config, 'development');
+	} catch (e) {
+		console.log('Error while trying to sync SvelteKit config');
+		console.log(e);
+	}
+}
diff --git a/packages/kit/scripts/extract-types.js b/packages/kit/scripts/extract-types.js
index f06110520..36a95b78d 100644
--- a/packages/kit/scripts/extract-types.js
+++ b/packages/kit/scripts/extract-types.js
@@ -22,6 +22,11 @@ function get_types(code, statements) {
 
 	if (statements) {
 		for (const statement of statements) {
+			const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
+
+			const export_modifier = modifiers?.find((modifier) => modifier.kind === 93);
+			if (!export_modifier) continue;
+
 			if (
 				ts.isClassDeclaration(statement) ||
 				ts.isInterfaceDeclaration(statement) ||
diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js
index 98348e6f0..6e931aaf9 100755
--- a/packages/kit/src/cli.js
+++ b/packages/kit/src/cli.js
@@ -41,10 +41,6 @@ prog
 			return;
 		}
 
-		if (event === 'postinstall' && process.env.INIT_CWD) {
-			process.chdir(process.env.INIT_CWD);
-		}
-
 		if (!fs.existsSync('svelte.config.js')) {
 			console.warn(`Missing ${path.resolve('svelte.config.js')} — skipping`);
 			return;
diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js
index d0e35d59f..91ec76997 100644
--- a/packages/kit/src/core/generate_manifest/index.js
+++ b/packages/kit/src/core/generate_manifest/index.js
@@ -1,5 +1,6 @@
 import { s } from '../../utils/misc.js';
 import { get_mime_lookup } from '../utils.js';
+import { resolve_symlinks } from '../../exports/vite/build/utils.js';
 
 /**
  * Generates the data used to write the server-side manifest.js file. This data is used in the Vite
@@ -65,7 +66,7 @@ export function generate_manifest({ build_data, relative_path, routes, format =
 					names: ${s(route.names)},
 					types: ${s(route.types)},
 					page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${route.page.leaf} }` : 'null'},
-					endpoint: ${route.endpoint ? loader(`${relative_path}/${build_data.server.vite_manifest[route.endpoint.file].file}`) : 'null'}
+					endpoint: ${route.endpoint ? loader(`${relative_path}/${resolve_symlinks(build_data.server.vite_manifest, route.endpoint.file).chunk.file}`) : 'null'}
 				}`;
 				}).filter(Boolean).join(',\n\t\t\t\t')}
 			],
diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js
index 563c391b0..852a02a86 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -164,13 +164,16 @@ function create_routes_and_nodes(cwd, config, fallback) {
 
 			const dir = path.join(cwd, routes_base, id);
 
-			const files = fs.readdirSync(dir, {
-				withFileTypes: true
-			});
+			// We can't use withFileTypes because of a NodeJs bug which returns wrong results
+			// with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
+			const files = fs.readdirSync(dir).map((name) => ({
+				is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
+				name
+			}));
 
 			// process files first
 			for (const file of files) {
-				if (file.isDirectory()) continue;
+				if (file.is_dir) continue;
 				if (!file.name.startsWith('+')) continue;
 				if (!valid_extensions.find((ext) => file.name.endsWith(ext))) continue;
 
@@ -213,7 +216,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
 
 			// then handle children
 			for (const file of files) {
-				if (file.isDirectory()) {
+				if (file.is_dir) {
 					walk(depth + 1, path.posix.join(id, file.name), file.name, route);
 				}
 			}
@@ -222,97 +225,115 @@ function create_routes_and_nodes(cwd, config, fallback) {
 		walk(0, '', '', null);
 
 		const root = /** @type {import('types').RouteData} */ (route_map.get(''));
-
-		// TODO remove for 1.0
 		if (route_map.size === 1) {
 			if (!root.leaf && !root.error && !root.layout && !root.endpoint) {
 				throw new Error(
+					// TODO adjust this error message for 1.0
+					// 'No routes found. If you are using a custom src/routes directory, make sure it is specified in svelte.config.js'
 					'The filesystem router API has changed, see https://github.com/sveltejs/kit/discussions/5774 for details'
 				);
 			}
 		}
+	} else {
+		// If there's no routes directory, we'll just create a single empty route. This ensures the root layout and
+		// error components are included in the manifest, which is needed for subsequent build/dev commands to work
+		route_map.set('', {
+			id: '',
+			segment: '',
+			pattern: /^$/,
+			names: [],
+			types: [],
+			parent: null,
+			layout: null,
+			error: null,
+			leaf: null,
+			page: null,
+			endpoint: null
+		});
+	}
 
-		if (!root.layout?.component) {
-			if (!root.layout) root.layout = { depth: 0, child_pages: [] };
-			root.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
-		}
+	const root = /** @type {import('types').RouteData} */ (route_map.get(''));
 
-		if (!root.error?.component) {
-			if (!root.error) root.error = { depth: 0 };
-			root.error.component = posixify(path.relative(cwd, `${fallback}/error.svelte`));
-		}
+	if (!root.layout?.component) {
+		if (!root.layout) root.layout = { depth: 0, child_pages: [] };
+		root.layout.component = posixify(path.relative(cwd, `${fallback}/layout.svelte`));
+	}
 
-		// we do layouts/errors first as they are more likely to be reused,
-		// and smaller indexes take fewer bytes. also, this guarantees that
-		// the default error/layout are 0/1
-		route_map.forEach((route) => {
-			if (route.layout) nodes.push(route.layout);
-			if (route.error) nodes.push(route.error);
-		});
+	if (!root.error?.component) {
+		if (!root.error) root.error = { depth: 0 };
+		root.error.component = posixify(path.relative(cwd, `${fallback}/error.svelte`));
+	}
 
-		/** @type {Map<string, string>} */
-		const conflicts = new Map();
+	// we do layouts/errors first as they are more likely to be reused,
+	// and smaller indexes take fewer bytes. also, this guarantees that
+	// the default error/layout are 0/1
+	route_map.forEach((route) => {
+		if (route.layout) nodes.push(route.layout);
+		if (route.error) nodes.push(route.error);
+	});
 
-		route_map.forEach((route) => {
-			if (!route.leaf) return;
+	/** @type {Map<string, string>} */
+	const conflicts = new Map();
 
-			nodes.push(route.leaf);
+	route_map.forEach((route) => {
+		if (!route.leaf) return;
 
-			const normalized = route.id.split('/').filter(affects_path).join('/');
+		nodes.push(route.leaf);
 
-			if (conflicts.has(normalized)) {
-				throw new Error(`${conflicts.get(normalized)} and ${route.id} occupy the same route`);
-			}
+		const normalized = route.id.split('/').filter(affects_path).join('/');
 
-			conflicts.set(normalized, route.id);
-		});
+		if (conflicts.has(normalized)) {
+			throw new Error(`${conflicts.get(normalized)} and ${route.id} occupy the same route`);
+		}
 
-		const indexes = new Map(nodes.map((node, i) => [node, i]));
+		conflicts.set(normalized, route.id);
+	});
 
-		route_map.forEach((route) => {
-			if (!route.leaf) return;
+	const indexes = new Map(nodes.map((node, i) => [node, i]));
 
-			route.page = {
-				layouts: [],
-				errors: [],
-				leaf: /** @type {number} */ (indexes.get(route.leaf))
-			};
+	route_map.forEach((route) => {
+		if (!route.leaf) return;
 
-			/** @type {import('types').RouteData | null} */
-			let current_route = route;
-			let current_node = route.leaf;
-			let parent_id = route.leaf.parent_id;
-
-			while (current_route) {
-				if (parent_id === undefined || current_route.segment === parent_id) {
-					if (current_route.layout || current_route.error) {
-						route.page.layouts.unshift(
-							current_route.layout ? indexes.get(current_route.layout) : undefined
-						);
-						route.page.errors.unshift(
-							current_route.error ? indexes.get(current_route.error) : undefined
-						);
-					}
+		route.page = {
+			layouts: [],
+			errors: [],
+			leaf: /** @type {number} */ (indexes.get(route.leaf))
+		};
 
-					if (current_route.layout) {
-						/** @type {import('types').PageNode[]} */ (current_route.layout.child_pages).push(
-							route.leaf
-						);
-						current_node.parent = current_node = current_route.layout;
-						parent_id = current_node.parent_id;
-					} else {
-						parent_id = undefined;
-					}
+		/** @type {import('types').RouteData | null} */
+		let current_route = route;
+		let current_node = route.leaf;
+		let parent_id = route.leaf.parent_id;
+
+		while (current_route) {
+			if (parent_id === undefined || current_route.segment === parent_id) {
+				if (current_route.layout || current_route.error) {
+					route.page.layouts.unshift(
+						current_route.layout ? indexes.get(current_route.layout) : undefined
+					);
+					route.page.errors.unshift(
+						current_route.error ? indexes.get(current_route.error) : undefined
+					);
 				}
 
-				current_route = current_route.parent;
+				if (current_route.layout) {
+					/** @type {import('types').PageNode[]} */ (current_route.layout.child_pages).push(
+						route.leaf
+					);
+					current_node.parent = current_node = current_route.layout;
+					parent_id = current_node.parent_id;
+				} else {
+					parent_id = undefined;
+				}
 			}
 
-			if (parent_id !== undefined) {
-				throw new Error(`${current_node.component} references missing segment "${parent_id}"`);
-			}
-		});
-	}
+			current_route = current_route.parent;
+		}
+
+		if (parent_id !== undefined) {
+			throw new Error(`${current_node.component} references missing segment "${parent_id}"`);
+		}
+	});
 
 	const routes = Array.from(route_map.values()).sort((a, b) => compare(a, b, segment_map));
 
diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js
index f1a863077..031071a4d 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js
@@ -173,8 +173,16 @@ test('creates routes with layout', () => {
 
 test('succeeds when routes does not exist', () => {
 	const { nodes, routes } = create('samples/basic/routes');
-	assert.equal(nodes, []);
-	assert.equal(routes, []);
+	assert.equal(nodes.map(simplify_node), [
+		{ component: 'layout.svelte' },
+		{ component: 'error.svelte' }
+	]);
+	assert.equal(routes.map(simplify_route), [
+		{
+			id: '',
+			pattern: '/^$/'
+		}
+	]);
 });
 
 // TODO some characters will need to be URL-encoded in the filename
diff --git a/packages/kit/src/core/sync/write_types/index.js b/packages/kit/src/core/sync/write_types/index.js
index 5204a8de5..a7c82f905 100644
--- a/packages/kit/src/core/sync/write_types/index.js
+++ b/packages/kit/src/core/sync/write_types/index.js
@@ -68,7 +68,7 @@ export async function write_types(config, manifest_data, file) {
 		return;
 	}
 
-	const id = path.posix.relative(config.kit.files.routes, path.dirname(file));
+	const id = posixify(path.relative(config.kit.files.routes, path.dirname(file)));
 
 	const route = manifest_data.routes.find((route) => route.id === id);
 	if (!route) return; // this shouldn't ever happen
diff --git a/packages/kit/src/exports/node/index.js b/packages/kit/src/exports/node/index.js
index b73c5c91b..67b82e63f 100644
--- a/packages/kit/src/exports/node/index.js
+++ b/packages/kit/src/exports/node/index.js
@@ -1,4 +1,5 @@
 import * as set_cookie_parser from 'set-cookie-parser';
+import { error } from '../index.js';
 
 /**
  * @param {import('http').IncomingMessage} req
@@ -27,7 +28,8 @@ function get_raw_body(req, body_size_limit) {
 		if (!length) {
 			length = body_size_limit;
 		} else if (length > body_size_limit) {
-			throw new Error(
+			throw error(
+				413,
 				`Received content-length of ${length}, but only accept up to ${body_size_limit} bytes.`
 			);
 		}
@@ -45,6 +47,7 @@ function get_raw_body(req, body_size_limit) {
 	return new ReadableStream({
 		start(controller) {
 			req.on('error', (error) => {
+				cancelled = true;
 				controller.error(error);
 			});
 
@@ -58,8 +61,10 @@ function get_raw_body(req, body_size_limit) {
 
 				size += chunk.length;
 				if (size > length) {
-					req.destroy(
-						new Error(
+					cancelled = true;
+					controller.error(
+						error(
+							413,
 							`request body size exceeded ${
 								content_length ? "'content-length'" : 'BODY_SIZE_LIMIT'
 							} of ${length}`
diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js
index 77011b11e..0f1120e5f 100644
--- a/packages/kit/src/exports/vite/build/build_server.js
+++ b/packages/kit/src/exports/vite/build/build_server.js
@@ -4,7 +4,13 @@ import { mkdirp, posixify, resolve_entry } from '../../../utils/filesystem.js';
 import { get_vite_config, merge_vite_configs } from '../utils.js';
 import { load_error_page, load_template } from '../../../core/config/index.js';
 import { runtime_directory } from '../../../core/utils.js';
-import { create_build, find_deps, get_default_build_config, is_http_method } from './utils.js';
+import {
+	create_build,
+	find_deps,
+	get_default_build_config,
+	is_http_method,
+	resolve_symlinks
+} from './utils.js';
 import { s } from '../../../utils/misc.js';
 
 /**
@@ -68,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: 'Internal Error' };
+				}) ?? { message: event.routeId ? 'Internal Error' : 'Not Found' };
 			},
 			hooks: null,
 			manifest,
@@ -76,7 +82,7 @@ export class Server {
 			public_env: {},
 			read,
 			root,
-			service_worker: ${has_service_worker ? "base + '/service-worker.js'" : 'null'},
+			service_worker: ${has_service_worker},
 			app_template,
 			app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')},
 			error_template,
@@ -285,7 +291,7 @@ export async function build_server(options, client) {
 
 			exports.push(
 				`export const component = async () => (await import('../${
-					vite_manifest[node.component].file
+					resolve_symlinks(vite_manifest, node.component).chunk.file
 				}')).default;`,
 				`export const file = '${entry.file}';` // TODO what is this?
 			);
diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js
index 4a0e19e4a..da3134c31 100644
--- a/packages/kit/src/exports/vite/build/utils.js
+++ b/packages/kit/src/exports/vite/build/utils.js
@@ -1,3 +1,5 @@
+import fs from 'fs';
+import path from 'path';
 import * as vite from 'vite';
 import { get_aliases } from '../utils.js';
 
@@ -44,14 +46,14 @@ export function find_deps(manifest, entry, add_dynamic_css) {
 	const stylesheets = new Set();
 
 	/**
-	 * @param {string} file
+	 * @param {string} current
 	 * @param {boolean} add_js
 	 */
-	function traverse(file, add_js) {
-		if (seen.has(file)) return;
-		seen.add(file);
+	function traverse(current, add_js) {
+		if (seen.has(current)) return;
+		seen.add(current);
 
-		const chunk = manifest[file];
+		const { chunk } = resolve_symlinks(manifest, current);
 
 		if (add_js) imports.add(chunk.file);
 
@@ -68,15 +70,31 @@ export function find_deps(manifest, entry, add_dynamic_css) {
 		}
 	}
 
-	traverse(entry, true);
+	const { chunk, file } = resolve_symlinks(manifest, entry);
+
+	traverse(file, true);
 
 	return {
-		file: manifest[entry].file,
+		file: chunk.file,
 		imports: Array.from(imports),
 		stylesheets: Array.from(stylesheets)
 	};
 }
 
+/**
+ * @param {import('vite').Manifest} manifest
+ * @param {string} file
+ */
+export function resolve_symlinks(manifest, file) {
+	while (!manifest[file]) {
+		file = path.relative('.', fs.realpathSync(file));
+	}
+
+	const chunk = manifest[file];
+
+	return { chunk, file };
+}
+
 /**
  * The Vite configuration that we use by default.
  * @param {{
diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js
index 3c28acb48..3c971ce19 100644
--- a/packages/kit/src/exports/vite/dev/index.js
+++ b/packages/kit/src/exports/vite/dev/index.js
@@ -350,7 +350,7 @@ export async function dev(vite, vite_config, svelte_config) {
 						user_hooks.handleError ||
 						(({ error: e }) => {
 							const error = /** @type {Error & { frame?: string }} */ (e);
-							console.error(colors.bold().red(error.message));
+							console.error(colors.bold().red(error.message ?? error)); // Could be anything
 							if (error.frame) {
 								console.error(colors.gray(error.frame));
 							}
@@ -397,7 +397,7 @@ export async function dev(vite, vite_config, svelte_config) {
 					});
 				} catch (/** @type {any} */ err) {
 					res.statusCode = err.status || 400;
-					return res.end(err.message || 'Invalid request body');
+					return res.end('Invalid request body');
 				}
 
 				const template = load_template(cwd, svelte_config);
@@ -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: 'Internal Error' }
+								}) ?? { message: event.routeId ? 'Internal Error' : 'Not Found' }
 							);
 						},
 						hooks,
@@ -460,6 +460,7 @@ export async function dev(vite, vite_config, svelte_config) {
 								.replace(/%sveltekit\.status%/g, String(status))
 								.replace(/%sveltekit\.error\.message%/g, message);
 						},
+						service_worker: false,
 						trailing_slash: svelte_config.kit.trailingSlash
 					},
 					{
diff --git a/packages/kit/src/exports/vite/preview/index.js b/packages/kit/src/exports/vite/preview/index.js
index 55a59105b..0a24a0b9c 100644
--- a/packages/kit/src/exports/vite/preview/index.js
+++ b/packages/kit/src/exports/vite/preview/index.js
@@ -137,7 +137,7 @@ export async function preview(vite, vite_config, svelte_config) {
 				});
 			} catch (/** @type {any} */ err) {
 				res.statusCode = err.status || 400;
-				return res.end(err.message || 'Invalid request body');
+				return res.end('Invalid request body');
 			}
 
 			setResponse(
diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js
index 6ab589caa..bf0601385 100644
--- a/packages/kit/src/runtime/app/forms.js
+++ b/packages/kit/src/runtime/app/forms.js
@@ -28,7 +28,13 @@ export function enhance(form, submit = () => {}) {
 			await invalidateAll();
 		}
 
-		if (location.origin + location.pathname === action.origin + action.pathname) {
+		// For success/invalid results, only apply action if it belongs to the
+		// current page, otherwise `form` will be updated erroneously
+		if (
+			location.origin + location.pathname === action.origin + action.pathname ||
+			result.type === 'redirect' ||
+			result.type === 'error'
+		) {
 			applyAction(result);
 		}
 	};
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 106b0c918..23046c111 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -73,11 +73,8 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {Array<((url: URL) => boolean)>} */
 	const invalidated = [];
 
-	/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
-	const load_cache = {
-		id: null,
-		promise: null
-	};
+	/** @type {{id: string, promise: Promise<import('./types').NavigationResult | undefined>} | null} */
+	let load_cache = null;
 
 	const callbacks = {
 		/** @type {Array<(navigation: import('types').Navigation & { cancel: () => void }) => void>} */
@@ -91,7 +88,6 @@ export function create_client({ target, base, trailing_slash }) {
 	let current = {
 		branch: [],
 		error: null,
-		session_id: 0,
 		// @ts-ignore - we need the initial value to be null
 		url: null
 	};
@@ -99,10 +95,6 @@ export function create_client({ target, base, trailing_slash }) {
 	let started = false;
 	let autoscroll = true;
 	let updating = false;
-	let session_id = 1;
-
-	/** @type {Promise<void> | null} */
-	let invalidating = null;
 	let force_invalidation = false;
 
 	/** @type {import('svelte').SvelteComponent} */
@@ -140,31 +132,38 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {{}} */
 	let token;
 
-	function invalidate() {
-		if (!invalidating) {
-			const url = new URL(location.href);
-
-			invalidating = Promise.resolve().then(async () => {
-				const intent = get_navigation_intent(url, true);
-				await update(intent, url, []);
-
-				invalidating = null;
-				force_invalidation = false;
-			});
-		}
-
-		return invalidating;
+	/** @type {Promise<void> | null} */
+	let pending_invalidate;
+
+	async function invalidate() {
+		// Accept all invalidations as they come, don't swallow any while another invalidation
+		// is running because subsequent invalidations may make earlier ones outdated,
+		// but batch multiple synchronous invalidations.
+		pending_invalidate = pending_invalidate || Promise.resolve();
+		await pending_invalidate;
+		pending_invalidate = null;
+
+		const url = new URL(location.href);
+		const intent = get_navigation_intent(url, true);
+		// Clear prefetch, it might be affected by the invalidation.
+		// Also solves an edge case where a prefetch is triggered, the navigation for it
+		// was then triggered and is still running while the invalidation kicks in,
+		// at which point the invalidation should take over and "win".
+		load_cache = null;
+		await update(intent, url, []);
 	}
 
 	/**
 	 * @param {string | URL} url
 	 * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
 	 * @param {string[]} redirect_chain
+	 * @param {{}} [nav_token]
 	 */
 	async function goto(
 		url,
 		{ noscroll = false, replaceState = false, keepfocus = false, state = {} },
-		redirect_chain
+		redirect_chain,
+		nav_token
 	) {
 		if (typeof url === 'string') {
 			url = new URL(url, get_base_uri(document));
@@ -179,6 +178,7 @@ export function create_client({ target, base, trailing_slash }) {
 				state,
 				replaceState
 			},
+			nav_token,
 			accepted: () => {},
 			blocked: () => {},
 			type: 'goto'
@@ -193,8 +193,7 @@ export function create_client({ target, base, trailing_slash }) {
 			throw new Error('Attempted to prefetch a URL that does not belong to this app');
 		}
 
-		load_cache.promise = load_route(intent);
-		load_cache.id = intent.id;
+		load_cache = { id: intent.id, promise: load_route(intent) };
 
 		return load_cache.promise;
 	}
@@ -205,10 +204,11 @@ export function create_client({ target, base, trailing_slash }) {
 	 * @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 {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token
 	 * @param {() => void} [callback]
 	 */
-	async function update(intent, url, redirect_chain, opts, callback) {
-		const current_token = (token = {});
+	async function update(intent, url, redirect_chain, opts, nav_token = {}, callback) {
+		token = nav_token;
 		let navigation_result = intent && (await load_route(intent));
 
 		if (
@@ -239,9 +239,7 @@ export function create_client({ target, base, trailing_slash }) {
 		url = intent?.url || url;
 
 		// abort if user navigated during update
-		if (token !== current_token) return false;
-
-		invalidated.length = 0;
+		if (token !== nav_token) return false;
 
 		if (navigation_result.type === 'redirect') {
 			if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
@@ -252,7 +250,12 @@ export function create_client({ target, base, trailing_slash }) {
 					routeId: null
 				});
 			} else {
-				goto(new URL(navigation_result.location, url).href, {}, [...redirect_chain, url.pathname]);
+				goto(
+					new URL(navigation_result.location, url).href,
+					{},
+					[...redirect_chain, url.pathname],
+					nav_token
+				);
 				return false;
 			}
 		} else if (navigation_result.props?.page?.status >= 400) {
@@ -262,6 +265,11 @@ export function create_client({ target, base, trailing_slash }) {
 			}
 		}
 
+		// reset invalidation only after a finished navigation. If there are redirects or
+		// additional invalidations, they should get the same invalidation treatment
+		invalidated.length = 0;
+		force_invalidation = false;
+
 		updating = true;
 
 		if (opts && opts.details) {
@@ -271,6 +279,9 @@ export function create_client({ target, base, trailing_slash }) {
 			history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
 		}
 
+		// reset prefetch synchronously after the history state has been set to avoid race conditions
+		load_cache = null;
+
 		if (started) {
 			current = navigation_result.state;
 
@@ -334,8 +345,6 @@ export function create_client({ target, base, trailing_slash }) {
 			await tick();
 		}
 
-		load_cache.promise = null;
-		load_cache.id = null;
 		autoscroll = true;
 
 		if (navigation_result.props.page) {
@@ -410,8 +419,7 @@ export function create_client({ target, base, trailing_slash }) {
 				params,
 				branch,
 				error,
-				route,
-				session_id
+				route
 			},
 			props: {
 				components: filtered.map((branch_node) => branch_node.node.component)
@@ -513,22 +521,15 @@ export function create_client({ target, base, trailing_slash }) {
 				}
 			}
 
-			/** @type {Record<string, string>} */
-			const uses_params = {};
-			for (const key in params) {
-				Object.defineProperty(uses_params, key, {
-					get() {
-						uses.params.add(key);
-						return params[key];
-					},
-					enumerable: true
-				});
-			}
-
 			/** @type {import('types').LoadEvent} */
 			const load_input = {
 				routeId,
-				params: uses_params,
+				params: new Proxy(params, {
+					get: (target, key) => {
+						uses.params.add(/** @type {string} */ (key));
+						return target[/** @type {string} */ (key)];
+					}
+				}),
 				data: server_data_node?.data ?? null,
 				url: make_trackable(url, () => {
 					uses.url = true;
@@ -536,9 +537,7 @@ export function create_client({ target, base, trailing_slash }) {
 				async fetch(resource, init) {
 					let requested;
 
-					if (typeof resource === 'string') {
-						requested = resource;
-					} else {
+					if (resource instanceof Request) {
 						requested = resource.url;
 
 						// we're not allowed to modify the received `Request` object, so in order
@@ -563,6 +562,8 @@ export function create_client({ target, base, trailing_slash }) {
 							signal: resource.signal,
 							...init
 						};
+					} else {
+						requested = resource;
 					}
 
 					// we must fixup relative urls so they are resolved from the target page
@@ -632,20 +633,21 @@ export function create_client({ target, base, trailing_slash }) {
 	}
 
 	/**
-	 * @param {import('types').Uses | undefined} uses
+	 * @param {boolean} url_changed
 	 * @param {boolean} parent_changed
-	 * @param {{ url: boolean, params: string[] }} changed
+	 * @param {import('types').Uses | undefined} uses
+	 * @param {Record<string, string>} params
 	 */
-	function has_changed(changed, parent_changed, uses) {
+	function has_changed(url_changed, parent_changed, uses, params) {
 		if (force_invalidation) return true;
 
 		if (!uses) return false;
 
 		if (uses.parent && parent_changed) return true;
-		if (changed.url && uses.url) return true;
+		if (uses.url && url_changed) return true;
 
-		for (const param of changed.params) {
-			if (uses.params.has(param)) return true;
+		for (const param of uses.params) {
+			if (params[param] !== current.params[param]) return true;
 		}
 
 		for (const href of uses.dependencies) {
@@ -683,17 +685,12 @@ export function create_client({ target, base, trailing_slash }) {
 	 * @returns {Promise<import('./types').NavigationResult | undefined>}
 	 */
 	async function load_route({ id, invalidating, url, params, route }) {
-		if (load_cache.id === id && load_cache.promise) {
+		if (load_cache?.id === id) {
 			return load_cache.promise;
 		}
 
 		const { errors, layouts, leaf } = route;
 
-		const changed = current.url && {
-			url: id !== current.url.pathname + current.url.search,
-			params: Object.keys(params).filter((key) => current.params[key] !== params[key])
-		};
-
 		const loaders = [...layouts, leaf];
 
 		// preload modules to avoid waterfall, but handle rejections
@@ -705,12 +702,15 @@ export function create_client({ target, base, trailing_slash }) {
 		/** @type {import('types').ServerData | null} */
 		let server_data = null;
 
+		const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
+
 		const invalid_server_nodes = loaders.reduce((acc, loader, i) => {
 			const previous = current.branch[i];
+
 			const invalid =
 				!!loader?.[0] &&
 				(previous?.loader !== loader[1] ||
-					has_changed(changed, acc.some(Boolean), previous.server?.uses));
+					has_changed(url_changed, acc.some(Boolean), previous.server?.uses, params));
 
 			acc.push(invalid);
 			return acc;
@@ -749,7 +749,7 @@ export function create_client({ target, base, trailing_slash }) {
 			const valid =
 				(!server_data_node || server_data_node.type === 'skip') &&
 				loader[1] === previous?.loader &&
-				!has_changed(changed, parent_changed, previous.shared?.uses);
+				!has_changed(url_changed, parent_changed, previous.shared?.uses, params);
 			if (valid) return previous;
 
 			parent_changed = true;
@@ -814,40 +814,22 @@ export function create_client({ target, base, trailing_slash }) {
 						error = handle_error(err, { params, url, routeId: route.id });
 					}
 
-					while (i--) {
-						if (errors[i]) {
-							/** @type {import('./types').BranchNode | undefined} */
-							let error_loaded;
-
-							let j = i;
-							while (!branch[j]) j -= 1;
-							try {
-								error_loaded = {
-									node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(),
-									loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]),
-									data: {},
-									server: null,
-									shared: null
-								};
-
-								return await get_navigation_result_from_branch({
-									url,
-									params,
-									branch: branch.slice(0, j + 1).concat(error_loaded),
-									status,
-									error,
-									route
-								});
-							} catch (e) {
-								continue;
-							}
-						}
+					const error_load = await load_nearest_error_page(i, branch, errors);
+					if (error_load) {
+						return await get_navigation_result_from_branch({
+							url,
+							params,
+							branch: branch.slice(0, error_load.idx).concat(error_load.node),
+							status,
+							error,
+							route
+						});
+					} 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;
 					}
-
-					// 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;
 				}
 			} else {
 				// push an empty slot so we can rewind past gaps to the
@@ -868,6 +850,35 @@ export function create_client({ target, base, trailing_slash }) {
 		});
 	}
 
+	/**
+	 * @param {number} i Start index to backtrack from
+	 * @param {Array<import('./types').BranchNode | undefined>} branch Branch to backtrack
+	 * @param {Array<import('types').CSRPageNodeLoader | undefined>} errors All error pages for this branch
+	 * @returns {Promise<{idx: number; node: import('./types').BranchNode} | undefined>}
+	 */
+	async function load_nearest_error_page(i, branch, errors) {
+		while (i--) {
+			if (errors[i]) {
+				let j = i;
+				while (!branch[j]) j -= 1;
+				try {
+					return {
+						idx: j + 1,
+						node: {
+							node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(),
+							loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]),
+							data: {},
+							server: null,
+							shared: null
+						}
+					};
+				} catch (e) {
+					continue;
+				}
+			}
+		}
+	}
+
 	/**
 	 * @param {{
 	 *   status: number;
@@ -981,6 +992,7 @@ export function create_client({ target, base, trailing_slash }) {
 	 *   } | null;
 	 *   type: import('types').NavigationType;
 	 *   delta?: number;
+	 *   nav_token?: {};
 	 *   accepted: () => void;
 	 *   blocked: () => void;
 	 * }} opts
@@ -993,6 +1005,7 @@ export function create_client({ target, base, trailing_slash }) {
 		details,
 		type,
 		delta,
+		nav_token,
 		accepted,
 		blocked
 	}) {
@@ -1050,6 +1063,7 @@ export function create_client({ target, base, trailing_slash }) {
 				keepfocus,
 				details
 			},
+			nav_token,
 			() => {
 				callbacks.after_navigate.forEach((fn) => fn(navigation));
 				stores.navigating.set(null);
@@ -1157,44 +1171,26 @@ export function create_client({ target, base, trailing_slash }) {
 				const { branch, route } = current;
 				if (!route) return;
 
-				let i = current.branch.length;
-
-				while (i--) {
-					if (route.errors[i]) {
-						/** @type {import('./types').BranchNode | undefined} */
-						let error_loaded;
-
-						let j = i;
-						while (!branch[j]) j -= 1;
-						try {
-							error_loaded = {
-								node: await /** @type {import('types').CSRPageNodeLoader } */ (route.errors[i])(),
-								loader: /** @type {import('types').CSRPageNodeLoader } */ (route.errors[i]),
-								data: {},
-								server: null,
-								shared: null
-							};
-
-							const navigation_result = await get_navigation_result_from_branch({
-								url,
-								params: current.params,
-								branch: branch.slice(0, j + 1).concat(error_loaded),
-								status: 500, // TODO might not be 500?
-								error: result.error,
-								route
-							});
-
-							current = navigation_result.state;
-
-							const post_update = pre_update();
-							root.$set(navigation_result.props);
-							post_update();
-
-							return;
-						} catch (e) {
-							continue;
-						}
-					}
+				const error_load = await load_nearest_error_page(
+					current.branch.length,
+					branch,
+					route.errors
+				);
+				if (error_load) {
+					const navigation_result = await get_navigation_result_from_branch({
+						url,
+						params: current.params,
+						branch: branch.slice(0, error_load.idx).concat(error_load.node),
+						status: 500, // TODO might not be 500?
+						error: result.error,
+						route
+					});
+
+					current = navigation_result.state;
+
+					const post_update = pre_update();
+					root.$set(navigation_result.props);
+					post_update();
 				}
 			} else if (result.type === 'redirect') {
 				goto(result.location, {}, []);
@@ -1203,10 +1199,8 @@ export function create_client({ target, base, trailing_slash }) {
 				const props = { form: result.data };
 
 				if (result.status !== page.status) {
-					props.page = {
-						...page,
-						status: result.status
-					};
+					page = { ...page, status: result.status };
+					props.page = page;
 				}
 
 				const post_update = pre_update();
@@ -1513,7 +1507,10 @@ async function load_data(url, invalid) {
  * @returns {App.PageError}
  */
 function handle_error(error, event) {
-	return hooks.handleError({ error, event }) ?? /** @type {any} */ ({ message: 'Internal Error' });
+	return (
+		hooks.handleError({ error, event }) ??
+		/** @type {any} */ ({ message: event.routeId ? 'Internal Error' : 'Not Found' })
+	);
 }
 
 // TODO remove for 1.0
@@ -1543,7 +1540,8 @@ function add_url_properties(type, target) {
 				throw new Error(
 					`The navigation shape changed - ${type}.${prop} should now be ${type}.url.${prop}`
 				);
-			}
+			},
+			enumerable: false
 		});
 	}
 
diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js
index 1552656fa..e355ee2db 100644
--- a/packages/kit/src/runtime/client/fetcher.js
+++ b/packages/kit/src/runtime/client/fetcher.js
@@ -62,12 +62,12 @@ const cache = new Map();
 /**
  * Should be called on the initial run of load functions that hydrate the page.
  * Saves any requests with cache-control max-age to the cache.
- * @param {RequestInfo} resource
+ * @param {RequestInfo | URL} resource
  * @param {string} resolved
  * @param {RequestInit} [opts]
  */
 export function initial_fetch(resource, resolved, opts) {
-	const url = JSON.stringify(typeof resource === 'string' ? resource : resource.url);
+	const url = JSON.stringify(resource instanceof Request ? resource.url : resource);
 
 	let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
 
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index a566a4ba2..4e88d9132 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -80,6 +80,5 @@ export interface NavigationState {
 	error: App.PageError | null;
 	params: Record<string, string>;
 	route: CSRRoute | null;
-	session_id: number;
 	url: URL;
 }
diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js
index 559dcce3e..69177f213 100644
--- a/packages/kit/src/runtime/server/cookie.js
+++ b/packages/kit/src/runtime/server/cookie.js
@@ -71,6 +71,18 @@ export function get_cookies(request, url) {
 					maxAge: 0
 				}
 			});
+		},
+
+		/**
+		 * @param {string} name
+		 * @param {string} value
+		 * @param {import('cookie').CookieSerializeOptions} opts
+		 */
+		serialize(name, value, opts) {
+			return serialize(name, value, {
+				...DEFAULT_SERIALIZE_OPTIONS,
+				...opts
+			});
 		}
 	};
 
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 9070608a0..7b1daadde 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -236,17 +236,6 @@ export async function respond(request, options, state) {
 					throw new Error('This should never happen');
 				}
 
-				if (!is_data_request) {
-					// we only want to set cookies on __data.js requests, we don't
-					// want to cache stuff erroneously etc
-					for (const key in headers) {
-						const value = headers[key];
-						response.headers.set(key, /** @type {string} */ (value));
-					}
-				}
-
-				add_cookies_to_headers(response.headers, Array.from(new_cookies.values()));
-
 				return response;
 			}
 
@@ -294,7 +283,21 @@ export async function respond(request, options, state) {
 	try {
 		const response = await options.hooks.handle({
 			event,
-			resolve,
+			resolve: (event, opts) =>
+				resolve(event, opts).then((response) => {
+					// add headers/cookies here, rather than inside `resolve`, so that we
+					// can do it once for all responses instead of once per `return`
+					if (!is_data_request) {
+						// we only want to set cookies on __data.js requests, we don't
+						// want to cache stuff erroneously etc
+						for (const key in headers) {
+							const value = headers[key];
+							response.headers.set(key, /** @type {string} */ (value));
+						}
+					}
+					add_cookies_to_headers(response.headers, Array.from(new_cookies.values()));
+					return response;
+				}),
 			// TODO remove for 1.0
 			// @ts-expect-error
 			get request() {
diff --git a/packages/kit/src/runtime/server/page/csp.js b/packages/kit/src/runtime/server/page/csp.js
index 16a060597..282369140 100644
--- a/packages/kit/src/runtime/server/page/csp.js
+++ b/packages/kit/src/runtime/server/page/csp.js
@@ -1,5 +1,5 @@
 import { escape_html_attr } from '../../../utils/escape.js';
-import { sha256, base64 } from './crypto.js';
+import { base64, sha256 } from './crypto.js';
 
 const array = new Uint8Array(16);
 
@@ -15,7 +15,8 @@ const quoted = new Set([
 	'unsafe-inline',
 	'none',
 	'strict-dynamic',
-	'report-sample'
+	'report-sample',
+	'wasm-unsafe-eval'
 ]);
 
 const crypto_pattern = /^(nonce|sha\d\d\d)-/;
diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js
index ec728254a..d84372930 100644
--- a/packages/kit/src/runtime/server/page/fetch.js
+++ b/packages/kit/src/runtime/server/page/fetch.js
@@ -68,6 +68,19 @@ export function create_fetch({ event, options, state, route, prerender_default,
 
 				const url = new URL(request.url);
 
+				if (!request.headers.has('origin')) {
+					request.headers.set('origin', event.url.origin);
+				}
+
+				// Remove Origin, according to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin#description
+				if (
+					(request.method === 'GET' || request.method === 'HEAD') &&
+					((request.mode === 'no-cors' && url.origin !== event.url.origin) ||
+						url.origin === event.url.origin)
+				) {
+					request.headers.delete('origin');
+				}
+
 				if (url.origin !== event.url.origin) {
 					// allow cookie passthrough for "same-origin"
 					// if SvelteKit is serving my.domain.com:
@@ -172,21 +185,21 @@ export function create_fetch({ event, options, state, route, prerender_default,
 					state.prerendering.dependencies.set(url.pathname, dependency);
 				}
 
+				const set_cookie = response.headers.get('set-cookie');
+				if (set_cookie) {
+					set_cookies.push(
+						...set_cookie_parser.splitCookiesString(set_cookie).map((str) => {
+							const { name, value, ...options } = set_cookie_parser.parseString(str);
+							// options.sameSite is string, something more specific is required - type cast is safe
+							return /** @type{import('./types').Cookie} */ ({ name, value, options });
+						})
+					);
+				}
+
 				return response;
 			}
 		});
 
-		const set_cookie = response.headers.get('set-cookie');
-		if (set_cookie) {
-			set_cookies.push(
-				...set_cookie_parser.splitCookiesString(set_cookie).map((str) => {
-					const { name, value, ...options } = set_cookie_parser.parseString(str);
-					// options.sameSite is string, something more specific is required - type cast is safe
-					return /** @type{import('./types').Cookie} */ ({ name, value, options });
-				})
-			);
-		}
-
 		const proxy = new Proxy(response, {
 			get(response, key, _receiver) {
 				async function text() {
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index 308d02e0c..2bb13b92d 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -187,37 +187,6 @@ export async function render_response({
 		serialized.form = devalue(form_value);
 	}
 
-	// prettier-ignore
-	const init_app = `
-		import { start } from ${s(prefixed(entry.file))};
-
-		start({
-			env: ${s(options.public_env)},
-			hydrate: ${page_config.ssr ? `{
-				status: ${status},
-				error: ${s(error)},
-				node_ids: [${branch.map(({ node }) => node.index).join(', ')}],
-				params: ${devalue(event.params)},
-				routeId: ${s(event.routeId)},
-				data: ${serialized.data},
-				form: ${serialized.form}
-			}` : 'null'},
-			paths: ${s(options.paths)},
-			target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode,
-			trailing_slash: ${s(options.trailing_slash)}
-		});
-	`;
-
-	// we use an anonymous function instead of an arrow function to support
-	// older browsers (https://github.com/sveltejs/kit/pull/5417)
-	const init_service_worker = `
-		if ('serviceWorker' in navigator) {
-			addEventListener('load', function () {
-				navigator.serviceWorker.register('${options.service_worker}');
-			});
-		}
-	`;
-
 	if (inline_styles.size > 0) {
 		const content = Array.from(inline_styles.values()).join('\n');
 
@@ -248,15 +217,36 @@ export async function render_response({
 		}
 
 		attributes.unshift('rel="stylesheet"');
-		head += `\n\t<link href="${path}" ${attributes.join(' ')}>`;
+		head += `\n\t\t<link href="${path}" ${attributes.join(' ')}>`;
 	}
 
 	if (page_config.csr) {
+		// prettier-ignore
+		const init_app = `
+			import { start } from ${s(prefixed(entry.file))};
+
+			start({
+				env: ${s(options.public_env)},
+				hydrate: ${page_config.ssr ? `{
+					status: ${status},
+					error: ${s(error)},
+					node_ids: [${branch.map(({ node }) => node.index).join(', ')}],
+					params: ${devalue(event.params)},
+					routeId: ${s(event.routeId)},
+					data: ${serialized.data},
+					form: ${serialized.form}
+				}` : 'null'},
+				paths: ${s(options.paths)},
+				target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode,
+				trailing_slash: ${s(options.trailing_slash)}
+			});
+		`;
+
 		for (const dep of modulepreloads) {
 			const path = prefixed(dep);
 			link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
 			if (state.prerendering) {
-				head += `\n\t<link rel="modulepreload" href="${path}">`;
+				head += `\n\t\t<link rel="modulepreload" href="${path}">`;
 			}
 		}
 
@@ -280,11 +270,21 @@ export async function render_response({
 	}
 
 	if (options.service_worker) {
+		// we use an anonymous function instead of an arrow function to support
+		// older browsers (https://github.com/sveltejs/kit/pull/5417)
+		const init_service_worker = `
+			if ('serviceWorker' in navigator) {
+				addEventListener('load', function () {
+					navigator.serviceWorker.register('${prefixed('service-worker.js')}');
+				});
+			}
+		`;
+
 		// always include service worker unless it's turned off explicitly
 		csp.add_script(init_service_worker);
 
 		head += `
-			<script${csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''}>${init_service_worker}</script>`;
+		<script${csp.script_needs_nonce ? ` nonce="${csp.nonce}"` : ''}>${init_service_worker}</script>`;
 	}
 
 	if (state.prerendering) {
diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js
index b0e790f02..76fb26f60 100644
--- a/packages/kit/src/runtime/server/page/respond_with_error.js
+++ b/packages/kit/src/runtime/server/page/respond_with_error.js
@@ -4,9 +4,11 @@ import {
 	handle_error_and_jsonify,
 	GENERIC_ERROR,
 	get_option,
-	static_error_page
+	static_error_page,
+	redirect_response
 } from '../utils.js';
 import { create_fetch } from './fetch.js';
+import { HttpError, Redirect } from '../../control.js';
 
 /**
  * @typedef {import('./types.js').Loaded} Loaded
@@ -87,6 +89,16 @@ export async function respond_with_error({ event, options, state, status, error,
 			resolve_opts
 		});
 	} catch (error) {
-		return static_error_page(options, 500, handle_error_and_jsonify(event, options, error).message);
+		// Edge case: If route is a 404 and the user redirects to somewhere from the root layout,
+		// we end up here.
+		if (error instanceof Redirect) {
+			return redirect_response(error.status, error.location, cookies);
+		}
+
+		return static_error_page(
+			options,
+			error instanceof HttpError ? error.status : 500,
+			handle_error_and_jsonify(event, options, error).message
+		);
 	}
 }
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index 333491328..c137f9f4f 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -70,21 +70,18 @@ export function allowed_methods(mod) {
 
 /** @param {any} data */
 export function data_response(data) {
+	const headers = {
+		'content-type': 'application/javascript',
+		'cache-control': 'private, no-store'
+	};
+
 	try {
-		return new Response(`window.__sveltekit_data = ${devalue(data)}`, {
-			headers: {
-				'content-type': 'application/javascript'
-			}
-		});
+		return new Response(`window.__sveltekit_data = ${devalue(data)}`, { headers });
 	} catch (e) {
 		const error = /** @type {any} */ (e);
 		const match = /\[(\d+)\]\.data\.(.+)/.exec(error.path);
 		const message = match ? `${error.message} (data.${match[2]})` : error.message;
-		return new Response(`throw new Error(${JSON.stringify(message)})`, {
-			headers: {
-				'content-type': 'application/javascript'
-			}
-		});
+		return new Response(`throw new Error(${JSON.stringify(message)})`, { headers });
 	}
 }
 
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index b26a0a932..c48dba8ad 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -13,7 +13,8 @@ import {
 	PrerenderOnErrorValue,
 	RequestOptions,
 	RouteDefinition,
-	TrailingSlash
+	TrailingSlash,
+	UniqueInterface
 } from './private.js';
 import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal.js';
 import { HttpError, Redirect } from '../src/runtime/control.js';
@@ -52,7 +53,8 @@ type OptionalUnion<
 > = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
 
 // Needs to be here, else ActionData will be resolved to unknown - probably because of "d.ts file imports .js file" in combination with allowJs
-interface ValidationError<T extends Record<string, unknown> | undefined = undefined> {
+export interface ValidationError<T extends Record<string, unknown> | undefined = undefined>
+	extends UniqueInterface {
 	status: number;
 	data: T;
 }
@@ -155,6 +157,19 @@ export interface Cookies {
 	 * Deletes a cookie by setting its value to an empty string and setting the expiry date in the past.
 	 */
 	delete(name: string, opts?: import('cookie').CookieSerializeOptions): void;
+
+	/**
+	 * Serialize a cookie name-value pair into a Set-Cookie header string.
+	 *
+	 * The `httpOnly` and `secure` options are `true` by default, and must be explicitly disabled if you want cookies to be readable by client-side JavaScript and/or transmitted over HTTP. The `sameSite` option defaults to `lax`.
+	 *
+	 * By default, the `path` of a cookie is the current pathname. In most cases you should explicitly set `path: '/'` to make the cookie available throughout your app.
+	 *
+	 * @param name the name for the cookie
+	 * @param value value to set the cookie to
+	 * @param options object containing serialization options
+	 */
+	serialize(name: string, value: string, opts?: import('cookie').CookieSerializeOptions): string;
 }
 
 export interface KitConfig {
@@ -250,7 +265,7 @@ export interface LoadEvent<
 	Data extends Record<string, unknown> | null = Record<string, any> | null,
 	ParentData extends Record<string, unknown> = Record<string, any>
 > extends NavigationEvent<Params> {
-	fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
+	fetch: typeof fetch;
 	data: Data;
 	setHeaders: (headers: Record<string, string>) => void;
 	parent: () => Promise<ParentData>;
@@ -368,6 +383,7 @@ export interface ServerLoadEvent<
 	ParentData extends Record<string, any> = Record<string, any>
 > extends RequestEvent<Params> {
 	parent: () => Promise<ParentData>;
+	depends: (...deps: string[]) => void;
 }
 
 export interface Action<
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index aebf0be71..29f84240d 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -298,7 +298,7 @@ export interface SSROptions {
 	public_env: Record<string, string>;
 	read(file: string): Buffer;
 	root: SSRComponent['default'];
-	service_worker?: string;
+	service_worker: boolean;
 	app_template({
 		head,
 		body,
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index 18a2c1091..36ed7cfbe 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -53,7 +53,13 @@ export interface AdapterEntry {
 
 export namespace Csp {
 	type ActionSource = 'strict-dynamic' | 'report-sample';
-	type BaseSource = 'self' | 'unsafe-eval' | 'unsafe-hashes' | 'unsafe-inline' | 'none';
+	type BaseSource =
+		| 'self'
+		| 'unsafe-eval'
+		| 'unsafe-hashes'
+		| 'unsafe-inline'
+		| 'wasm-unsafe-eval'
+		| 'none';
 	type CryptoSource = `${'nonce' | 'sha256' | 'sha384' | 'sha512'}-${string}`;
 	type FrameSource = HostSource | SchemeSource | 'self' | 'none';
 	type HostNameScheme = `${string}.${string}` | 'localhost';
@@ -207,3 +213,12 @@ export interface RouteSegment {
 }
 
 export type TrailingSlash = 'never' | 'always' | 'ignore';
+
+/**
+ * This doesn't actually exist, it's a way to better distinguish the type
+ */
+declare const uniqueSymbol: unique symbol;
+
+export interface UniqueInterface {
+	readonly [uniqueSymbol]: unknown;
+}
diff --git a/packages/package/CHANGELOG.md b/packages/package/CHANGELOG.md
index 0fbb8b088..2d6c7cb57 100644
--- a/packages/package/CHANGELOG.md
+++ b/packages/package/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/package
 
+## 1.0.0-next.5
+
+### Patch Changes
+
+- fix check for undefined on application/ strip ([#6932](https://github.com/sveltejs/kit/pull/6932))
+
 ## 1.0.0-next.4
 
 ### Patch Changes
diff --git a/packages/package/package.json b/packages/package/package.json
index b5bf86863..731fa5ec7 100644
--- a/packages/package/package.json
+++ b/packages/package/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/package",
-	"version": "1.0.0-next.4",
+	"version": "1.0.0-next.5",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/package/src/utils.js b/packages/package/src/utils.js
index 0b263857b..001970610 100644
--- a/packages/package/src/utils.js
+++ b/packages/package/src/utils.js
@@ -52,7 +52,7 @@ export function strip_lang_tags(content) {
 		.replace(
 			/(<!--[^]*?-->)|(<script[^>]*?)\s(?:type|lang)=(["'])(.*?)\3/g,
 			// things like application/ld+json should be kept as-is. Preprocessed languages are "ts" etc
-			(match, s1, s2, _, s4) => (s4.startsWith('application/') ? match : (s1 ?? '') + (s2 ?? ''))
+			(match, s1, s2, _, s4) => (s4?.startsWith('application/') ? match : (s1 ?? '') + (s2 ?? ''))
 		)
 		.replace(/(<!--[^]*?-->)|(<style[^>]*?)\s(?:type|lang)=(["']).*?\3/g, '$1$2');
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4f29e6486..f1a309a23 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -114,11 +114,12 @@ importers:
 
   packages/adapter-node:
     specifiers:
+      '@rollup/plugin-commonjs': ^22.0.1
       '@rollup/plugin-json': ^4.1.0
+      '@rollup/plugin-node-resolve': ^14.1.0
       '@sveltejs/kit': workspace:*
       '@types/node': ^16.11.36
       c8: ^7.11.3
-      esbuild: ^0.15.7
       node-fetch: ^3.2.4
       polka: ^1.0.0-next.22
       rimraf: ^3.0.2
@@ -127,16 +128,17 @@ importers:
       typescript: ^4.8.2
       uvu: ^0.5.3
     dependencies:
-      esbuild: 0.15.7
-    devDependencies:
+      '@rollup/plugin-commonjs': 22.0.1_rollup@2.78.1
       '@rollup/plugin-json': 4.1.0_rollup@2.78.1
+      '@rollup/plugin-node-resolve': 14.1.0_rollup@2.78.1
+      rollup: 2.78.1
+    devDependencies:
       '@sveltejs/kit': link:../kit
       '@types/node': 16.11.42
       c8: 7.11.3
       node-fetch: 3.2.6
       polka: 1.0.0-next.22
       rimraf: 3.0.2
-      rollup: 2.78.1
       sirv: 2.0.2
       typescript: 4.8.2
       uvu: 0.5.4
@@ -1085,9 +1087,8 @@ packages:
       glob: 7.2.3
       is-reference: 1.2.1
       magic-string: 0.25.9
-      resolve: 1.22.0
+      resolve: 1.22.1
       rollup: 2.78.1
-    dev: true
 
   /@rollup/plugin-json/4.1.0_rollup@2.78.1:
     resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==}
@@ -1096,7 +1097,6 @@ packages:
     dependencies:
       '@rollup/pluginutils': 3.1.0_rollup@2.78.1
       rollup: 2.78.1
-    dev: true
 
   /@rollup/plugin-node-resolve/14.1.0_rollup@2.78.1:
     resolution: {integrity: sha512-5G2niJroNCz/1zqwXtk0t9+twOSDlG00k1Wfd7bkbbXmwg8H8dvgHdIWAun53Ps/rckfvOC7scDBjuGFg5OaWw==}
@@ -1111,7 +1111,6 @@ packages:
       is-module: 1.0.0
       resolve: 1.22.1
       rollup: 2.78.1
-    dev: true
 
   /@rollup/pluginutils/3.1.0_rollup@2.78.1:
     resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
@@ -1123,7 +1122,6 @@ packages:
       estree-walker: 1.0.1
       picomatch: 2.3.1
       rollup: 2.78.1
-    dev: true
 
   /@rollup/pluginutils/4.2.1:
     resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
@@ -1183,11 +1181,9 @@ packages:
 
   /@types/estree/0.0.39:
     resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
-    dev: true
 
   /@types/estree/0.0.52:
     resolution: {integrity: sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ==}
-    dev: true
 
   /@types/gitignore-parser/0.0.0:
     resolution: {integrity: sha512-qxOKILdhl4e639fWdkMySS4tBkRYHkrU2ZNScsMu84EPicliFRr+gAXCLPrs7kTFWdDpgAIlxtUr+YCRtVjsKw==}
@@ -1229,7 +1225,6 @@ packages:
 
   /@types/node/18.0.0:
     resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==}
-    dev: true
 
   /@types/normalize-package-data/2.4.1:
     resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@@ -1253,7 +1248,6 @@ packages:
     resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
     dependencies:
       '@types/node': 18.0.0
-    dev: true
 
   /@types/sade/1.7.4:
     resolution: {integrity: sha512-6ys13kmtlY0aIOz4KtMdeBD9BHs6vSE3aRcj4vAZqXjypT2el8WZt6799CMjElVgh1cbOH/t3vrpQ4IpwytcPA==}
@@ -1504,7 +1498,6 @@ packages:
   /builtin-modules/3.3.0:
     resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
     engines: {node: '>=6'}
-    dev: true
 
   /c8/7.11.3:
     resolution: {integrity: sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==}
@@ -1693,7 +1686,6 @@ packages:
 
   /commondir/1.0.1:
     resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
-    dev: true
 
   /concat-map/0.0.1:
     resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
@@ -2168,7 +2160,6 @@ packages:
 
   /estree-walker/1.0.1:
     resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
-    dev: true
 
   /estree-walker/2.0.2:
     resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -2622,7 +2613,6 @@ packages:
     engines: {node: '>=6'}
     dependencies:
       builtin-modules: 3.3.0
-    dev: true
 
   /is-callable/1.2.4:
     resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==}
@@ -2641,12 +2631,6 @@ packages:
     dependencies:
       has: 1.0.3
 
-  /is-core-module/2.9.0:
-    resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==}
-    dependencies:
-      has: 1.0.3
-    dev: true
-
   /is-date-object/1.0.5:
     resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
     engines: {node: '>= 0.4'}
@@ -2682,7 +2666,6 @@ packages:
 
   /is-module/1.0.0:
     resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
-    dev: true
 
   /is-negative-zero/2.0.2:
     resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
@@ -2713,7 +2696,6 @@ packages:
     resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
     dependencies:
       '@types/estree': 0.0.52
-    dev: true
 
   /is-regex/1.1.4:
     resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
@@ -2922,7 +2904,6 @@ packages:
     resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
     dependencies:
       sourcemap-codec: 1.4.8
-    dev: true
 
   /magic-string/0.26.2:
     resolution: {integrity: sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==}
@@ -3658,15 +3639,6 @@ packages:
     deprecated: https://github.com/lydell/resolve-url#deprecated
     dev: true
 
-  /resolve/1.22.0:
-    resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
-    hasBin: true
-    dependencies:
-      is-core-module: 2.9.0
-      path-parse: 1.0.7
-      supports-preserve-symlinks-flag: 1.0.0
-    dev: true
-
   /resolve/1.22.1:
     resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
     hasBin: true


Test patch:
diff --git a/packages/kit/test/apps/basics/.gitignore b/packages/kit/test/apps/basics/.gitignore
index fa2f5a579..fad4d3e15 100644
--- a/packages/kit/test/apps/basics/.gitignore
+++ b/packages/kit/test/apps/basics/.gitignore
@@ -1,2 +1,3 @@
 /test/errors.json
 !/.env
+/src/routes/routing/symlink-from
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json
index c3da79b25..76e9cf5b7 100644
--- a/packages/kit/test/apps/basics/package.json
+++ b/packages/kit/test/apps/basics/package.json
@@ -7,7 +7,7 @@
 		"build": "vite build",
 		"preview": "vite preview",
 		"check": "svelte-kit sync && tsc && svelte-check",
-		"test": "npm run test:dev && npm run test:build",
+		"test": "node test/setup.js && npm run test:dev && npm run test:build",
 		"test:dev": "rimraf test/errors.json && cross-env DEV=true playwright test",
 		"test:build": "rimraf test/errors.json && playwright test"
 	},
diff --git a/packages/kit/test/apps/basics/src/hooks.client.js b/packages/kit/test/apps/basics/src/hooks.client.js
index 62ee8c1eb..09bfd498b 100644
--- a/packages/kit/test/apps/basics/src/hooks.client.js
+++ b/packages/kit/test/apps/basics/src/hooks.client.js
@@ -1,4 +1,6 @@
 /** @type{import("@sveltejs/kit").HandleClientError} */
-export function handleError({ error }) {
-	return { message: /** @type {Error} */ (error).message };
+export function handleError({ error, event }) {
+	return event.url.pathname.endsWith('404-fallback')
+		? undefined
+		: { message: /** @type {Error} */ (error).message };
 }
diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js
index cb085363b..18f1c7a03 100644
--- a/packages/kit/test/apps/basics/src/hooks.server.js
+++ b/packages/kit/test/apps/basics/src/hooks.server.js
@@ -33,7 +33,7 @@ export const handleError = ({ event, error: e }) => {
 		: {};
 	errors[event.url.pathname] = error_to_pojo(error);
 	fs.writeFileSync('test/errors.json', JSON.stringify(errors));
-	return { message: error.message };
+	return event.url.pathname.endsWith('404-fallback') ? undefined : { message: error.message };
 };
 
 export const handle = sequence(
@@ -47,6 +47,15 @@ export const handle = sequence(
 		event.locals.name = /** @type {string} */ (event.cookies.get('name'));
 		return resolve(event);
 	},
+	async ({ event, resolve }) => {
+		if (event.url.pathname === '/cookies/serialize') {
+			event.cookies.set('before', 'before');
+			const response = await resolve(event);
+			response.headers.append('set-cookie', event.cookies.serialize('after', 'after'));
+			return response;
+		}
+		return resolve(event);
+	},
 	async ({ event, resolve }) => {
 		if (event.url.pathname === '/errors/error-in-handle') {
 			throw new Error('Error in handle');
diff --git a/packages/kit/test/apps/basics/src/routes/+layout.server.js b/packages/kit/test/apps/basics/src/routes/+layout.server.js
index cb10abf04..001350db5 100644
--- a/packages/kit/test/apps/basics/src/routes/+layout.server.js
+++ b/packages/kit/test/apps/basics/src/routes/+layout.server.js
@@ -1,6 +1,8 @@
-let should_fail = false;
+import { error, redirect } from '@sveltejs/kit';
+
+let should_fail = '';
 /**
- * @param {boolean} value
+ * @param {string} value
  */
 export function set_should_fail(value) {
 	should_fail = value;
@@ -8,8 +10,16 @@ export function set_should_fail(value) {
 
 export async function load() {
 	if (should_fail) {
-		set_should_fail(false);
-		throw new Error('Failed to load');
+		if (should_fail === 'expected') {
+			set_should_fail('');
+			throw error(401, 'Not allowed');
+		} else if (should_fail === 'unexpected') {
+			set_should_fail('');
+			throw new Error('Failed to load');
+		} else {
+			set_should_fail('');
+			throw redirect(307, '/load');
+		}
 	}
 	// Do NOT make this load function depend on something which would cause it to rerun
 	return {
diff --git a/packages/kit/test/apps/basics/src/routes/actions/redirect/+page.server.js b/packages/kit/test/apps/basics/src/routes/actions/redirect/+page.server.js
new file mode 100644
index 000000000..ff16ad14f
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/actions/redirect/+page.server.js
@@ -0,0 +1,15 @@
+import { redirect } from '@sveltejs/kit';
+
+/** @type {import('./$types').PageServerLoad} */
+export function load() {
+	return {
+		initial: 'initial'
+	};
+}
+
+/** @type {import('./$types').Actions} */
+export const actions = {
+	default: async () => {
+		throw redirect(303, '/actions/enhance');
+	}
+};
diff --git a/packages/kit/test/apps/basics/src/routes/actions/redirect/+page.svelte b/packages/kit/test/apps/basics/src/routes/actions/redirect/+page.svelte
new file mode 100644
index 000000000..7bcb55f72
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/actions/redirect/+page.svelte
@@ -0,0 +1,7 @@
+<script>
+	import { enhance } from '$app/forms';
+</script>
+
+<form method="post" use:enhance>
+	<button>Submit</button>
+</form>
diff --git a/packages/kit/test/apps/basics/src/routes/cookies/serialize/+server.js b/packages/kit/test/apps/basics/src/routes/cookies/serialize/+server.js
new file mode 100644
index 000000000..f46459823
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/cookies/serialize/+server.js
@@ -0,0 +1,8 @@
+/** @type {import('@sveltejs/kit').RequestHandler} */
+export const GET = ({ cookies }) => {
+	const response = new Response('success', {
+		status: 200
+	});
+	response.headers.append('set-cookie', cookies.serialize('endpoint', 'endpoint'));
+	return response;
+};
diff --git a/packages/kit/test/apps/basics/src/routes/errors/error-html/+page.svelte b/packages/kit/test/apps/basics/src/routes/errors/error-html/+page.svelte
index 2fda2e2e0..b8cf7fcdf 100644
--- a/packages/kit/test/apps/basics/src/routes/errors/error-html/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/errors/error-html/+page.svelte
@@ -1,8 +1,15 @@
 <script>
-	async function fail() {
-		await fetch('./error-html/make-root-fail');
-		location.reload();
+	/** @param {string} type */
+	async function fail(type) {
+		await fetch(`/errors/error-html/make-root-fail?type=${type}`);
+		if (type === 'redirect') {
+			location.assign(location.href + '/404');
+		} else {
+			location.reload();
+		}
 	}
 </script>
 
-<button on:click={fail}>Let's fail catastrophically</button>
+<button on:click={() => fail('unexpected')}>Unexpected</button>
+<button on:click={() => fail('expected')}>Expected</button>
+<button on:click={() => fail('redirect')}>Redirect</button>
diff --git a/packages/kit/test/apps/basics/src/routes/errors/error-html/make-root-fail/+server.js b/packages/kit/test/apps/basics/src/routes/errors/error-html/make-root-fail/+server.js
index 7c3f14cfd..c3030a9c9 100644
--- a/packages/kit/test/apps/basics/src/routes/errors/error-html/make-root-fail/+server.js
+++ b/packages/kit/test/apps/basics/src/routes/errors/error-html/make-root-fail/+server.js
@@ -1,6 +1,9 @@
 import { set_should_fail } from '../../../+layout.server';
 
-export function GET() {
-	set_should_fail(true);
+/**
+ * @type {import('./$types').RequestHandler} param0
+ */
+export function GET({ url }) {
+	set_should_fail(url.searchParams.get('type'));
 	return new Response();
 }
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-external-no-cookies/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-external-no-cookies/+page.js
new file mode 100644
index 000000000..a968114e2
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-external-no-cookies/+page.js
@@ -0,0 +1,7 @@
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch, url }) {
+	const port = url.searchParams.get('port');
+	const res = await fetch(`http://localhost:${port}`);
+
+	return await res.json();
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-external-no-cookies/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-external-no-cookies/+page.svelte
new file mode 100644
index 000000000..6e7c1e6ac
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-external-no-cookies/+page.svelte
@@ -0,0 +1,1 @@
+<h1>hello</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-origin-external/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-external/+page.js
new file mode 100644
index 000000000..35b8df449
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-external/+page.js
@@ -0,0 +1,8 @@
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch, url }) {
+	const port = url.searchParams.get('port');
+	const res = await fetch(`http://localhost:${port}`);
+
+	const { origin } = await res.json();
+	return { origin };
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-origin-external/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-external/+page.svelte
new file mode 100644
index 000000000..aa194cc01
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-external/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>origin: {data.origin}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/+page.js
new file mode 100644
index 000000000..a8a830652
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/+page.js
@@ -0,0 +1,10 @@
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch }) {
+	const res = await fetch('/load/fetch-origin-internal/resource', {
+		method: 'POST'
+	});
+
+	const { origin } = await res.json();
+
+	return { origin };
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/+page.svelte
new file mode 100644
index 000000000..aa194cc01
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/+page.svelte
@@ -0,0 +1,6 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>origin: {data.origin}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/resource/+server.js b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/resource/+server.js
new file mode 100644
index 000000000..ad1c6605f
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-origin-internal/resource/+server.js
@@ -0,0 +1,7 @@
+import { json } from '@sveltejs/kit';
+
+/** @type {import('./$types').RequestHandler} */
+export function POST({ request }) {
+	const origin = request.headers.get('origin');
+	return json({ origin });
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.js
new file mode 100644
index 000000000..2d4c6d175
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.js
@@ -0,0 +1,8 @@
+/** @type {import('./$types').PageLoad} */
+export function load({ data, depends }) {
+	depends('invalidate-depends:shared');
+	return {
+		shared: new Date().getTime(),
+		...data
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.server.js
new file mode 100644
index 000000000..fb81ddb99
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.server.js
@@ -0,0 +1,7 @@
+/** @type {import('./$types').PageServerLoad} */
+export function load({ depends }) {
+	depends('invalidate-depends:server');
+	return {
+		server: new Date().getTime()
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.svelte
new file mode 100644
index 000000000..1ce33105c
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/depends/+page.svelte
@@ -0,0 +1,24 @@
+<script>
+	import { invalidate } from '$app/navigation';
+
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<p class="shared">{data.shared}</p>
+<button
+	type="button"
+	class="shared"
+	on:click={() => invalidate('invalidate-depends:shared')}
+>
+	invalidate shared
+</button>
+
+<p class="server">{data.server}</p>
+<button
+	type="button"
+	class="server"
+	on:click={() => invalidate('invalidate-depends:server')}
+>
+	invalidate server
+</button>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+layout.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+layout.js
new file mode 100644
index 000000000..612afdb02
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+layout.js
@@ -0,0 +1,24 @@
+import { redirect } from '@sveltejs/kit';
+import { get } from 'svelte/store';
+import { get_layout, redirect_state } from './state';
+
+/** @type {import('./$types').LayoutLoad} */
+export function load({ depends }) {
+	depends('invalid:layout');
+
+	if (get(redirect_state) === 'running') {
+		redirect_state.set('done');
+		throw redirect(307, '/load/invalidation/multiple/redirect');
+	}
+
+	return new Promise((resolve) =>
+		setTimeout(
+			() =>
+				resolve({
+					count_layout: get_layout(),
+					redirect_mode: get(redirect_state)
+				}),
+			Math.random() * 500
+		)
+	);
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+layout.svelte
new file mode 100644
index 000000000..f9cc3bd4b
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+layout.svelte
@@ -0,0 +1,29 @@
+<script>
+	import { invalidate, invalidateAll } from '$app/navigation';
+	import { page } from '$app/stores';
+	import { increment_layout, increment_page } from './state';
+
+	/** @param {string} str */
+	async function update(str) {
+		if (str !== 'page') {
+			increment_layout();
+		}
+		if (str !== 'layout') {
+			increment_page();
+		}
+
+		if (str === 'all') {
+			invalidateAll();
+		} else {
+			invalidate(`invalid:${str}`);
+		}
+	}
+</script>
+
+<button class="layout" on:click={() => update('layout')}>Invalidate layout</button>
+<button class="page" on:click={() => update('page')}>Invalidate page</button>
+<button class="all" on:click={() => update('all')}>Invalidate all</button>
+
+<p>layout: {$page.data.count_layout}, page: {$page.data.count_page}</p>
+
+<slot />
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+page.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+page.js
new file mode 100644
index 000000000..3182fe7ef
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+page.js
@@ -0,0 +1,15 @@
+import { get_page } from './state';
+
+/** @type {import('./$types').PageLoad} */
+export function load({ depends }) {
+	depends('invalid:page');
+	return new Promise((resolve) =>
+		setTimeout(
+			() =>
+				resolve({
+					count_page: get_page()
+				}),
+			Math.random() * 500
+		)
+	);
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.js
new file mode 100644
index 000000000..db2ffca2a
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.js
@@ -0,0 +1,14 @@
+import { redirect } from '@sveltejs/kit';
+import { redirect_state } from '../state';
+
+/** @type {import('./$types').PageLoad} */
+export async function load({ parent }) {
+	const { redirect_mode } = await parent();
+	if (redirect_mode === 'start') {
+		redirect_state.set('running');
+		throw redirect(307, '/load/invalidation/multiple');
+	}
+	if (redirect_mode === 'running') {
+		throw new Error('Shouldnt get redirected here with state "running"');
+	}
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte
new file mode 100644
index 000000000..bb8eb9ccb
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/redirect/+page.svelte
@@ -0,0 +1,12 @@
+<script>
+	import { invalidateAll } from '$app/navigation';
+	import { redirect_state } from '../state';
+
+	function redirect() {
+		redirect_state.set('start');
+		invalidateAll();
+	}
+</script>
+
+<button class="redirect" on:click={redirect}>redirect</button>
+<p class="redirect-state">Redirect state: {$redirect_state}</p>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/state.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/state.js
new file mode 100644
index 000000000..3d1ca87a3
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/multiple/state.js
@@ -0,0 +1,22 @@
+import { writable } from 'svelte/store';
+
+let count_layout = 0;
+let count_page = 0;
+
+export const redirect_state = writable('initial');
+
+export function increment_layout() {
+	count_layout++;
+}
+
+export function increment_page() {
+	count_page++;
+}
+
+export function get_layout() {
+	return count_layout;
+}
+
+export function get_page() {
+	return count_page;
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.js b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.js
new file mode 100644
index 000000000..1d25f6234
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.js
@@ -0,0 +1,7 @@
+/** @type {import('./$types').LayoutLoad} */
+export function load({ params }) {
+	return {
+		a: params.a,
+		b: params.b
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.svelte
new file mode 100644
index 000000000..9b3472274
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+layout.svelte
@@ -0,0 +1,14 @@
+<script>
+	/** @type {import('./$types').LayoutData} */
+	export let data;
+</script>
+
+<slot />
+
+<pre>{JSON.stringify({
+		a: data.a,
+		b: data.b
+	})}</pre>
+
+<a href="/load/invalidation/params/1">1</a>
+<a href="/load/invalidation/params/1/x">1/x</a>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/+page.svelte
new file mode 100644
index 000000000..465276da1
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/+page.svelte
@@ -0,0 +1,1 @@
+<h1>[a]</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/[b]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/[b]/+page.svelte
new file mode 100644
index 000000000..956ca34c9
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/params/[a]/[b]/+page.svelte
@@ -0,0 +1,1 @@
+<h1>[a]/[b]</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/load/server-data-nostore/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/server-data-nostore/+page.server.js
new file mode 100644
index 000000000..01e1a121a
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/server-data-nostore/+page.server.js
@@ -0,0 +1,6 @@
+/** @type {import('./$types').PageServerLoad} */
+export function load({ url }) {
+	return {
+		x: url.searchParams.get('x')
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/server-data-nostore/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/server-data-nostore/+page.svelte
new file mode 100644
index 000000000..9dc41ad6f
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/server-data-nostore/+page.svelte
@@ -0,0 +1,7 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>x: {data.x}</h1>
+<a href="/load/server-data-nostore?x=2">2</a>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
index 61f62ec6f..1d300bf63 100644
--- a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
@@ -6,6 +6,7 @@
 
 <a href="/routing/a">a</a>
 <a href="/routing/ambiguous/ok.json" rel="external">ok</a>
+<a href="/routing/symlink-from">symlinked</a>
 <a href="http://localhost:{$page.url.searchParams.get('port')}">elsewhere</a>
 <a href="/static.json">static.json</a>
 
diff --git a/packages/kit/test/apps/basics/src/routes/routing/symlink-to/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/symlink-to/+page.svelte
new file mode 100644
index 000000000..d80b82ddf
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/symlink-to/+page.svelte
@@ -0,0 +1,1 @@
+<h1>symlinked</h1>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 42dedf5b7..9f8a5ed62 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -263,7 +263,7 @@ test.describe('Endpoints', () => {
 	});
 });
 
-test.describe('Errors', () => {
+test.describe.serial('Errors', () => {
 	test('client-side load errors', async ({ page }) => {
 		await page.goto('/errors/load-client');
 
@@ -302,14 +302,29 @@ test.describe('Errors', () => {
 		expect(await page.innerHTML('h1')).toBe('401');
 	});
 
-	test('Root error falls back to error.html', async ({ page }) => {
+	test('Root error falls back to error.html (unexpected error)', async ({ page }) => {
 		await page.goto('/errors/error-html');
-		await page.click('button');
+		await page.click('button:text-is("Unexpected")');
 		expect(await page.textContent('h1')).toBe('Error - 500');
 		expect(await page.textContent('p')).toBe(
 			'This is the static error page with the following message: Failed to load'
 		);
 	});
+
+	test('Root error falls back to error.html (expected error)', async ({ page }) => {
+		await page.goto('/errors/error-html');
+		await page.click('button:text-is("Expected")');
+		expect(await page.textContent('h1')).toBe('Error - 401');
+		expect(await page.textContent('p')).toBe(
+			'This is the static error page with the following message: Not allowed'
+		);
+	});
+
+	test('Root 404 redirects somewhere due to root layout', async ({ page, baseURL }) => {
+		await page.goto('/errors/error-html');
+		await Promise.all([page.waitForNavigation(), page.click('button:text-is("Redirect")')]);
+		expect(page.url()).toBe(baseURL + '/load');
+	});
 });
 
 test.describe('Load', () => {
@@ -416,6 +431,17 @@ test.describe('Load', () => {
 		await expect(page.locator('p')).toHaveText('Count is 2');
 	});
 
+	test('__data.js has cache-control: private, no-store', async ({ page, clicknav }) => {
+		await page.goto('/load/server-data-nostore?x=1');
+
+		const [response] = await Promise.all([
+			page.waitForResponse((response) => /__data\.js/.test(response.url())),
+			clicknav('[href="/load/server-data-nostore?x=2"]')
+		]);
+
+		expect(response.headers()['cache-control']).toBe('private, no-store');
+	});
+
 	if (process.env.DEV) {
 		test('using window.fetch causes a warning', async ({ page }) => {
 			const port = 5173;
@@ -734,6 +760,72 @@ test.describe.serial('Invalidation', () => {
 		await page.waitForTimeout(200);
 		expect(await page.textContent('h1')).toBe('a: 4, b: 5');
 	});
+
+	test('multiple invalidations run concurrently', async ({ page, request }) => {
+		await page.goto('/load/invalidation/multiple');
+		expect(await page.textContent('p')).toBe('layout: 0, page: 0');
+
+		await page.click('button.layout');
+		await page.click('button.layout');
+		await page.click('button.page');
+		await page.click('button.page');
+		await page.click('button.layout');
+		await page.click('button.page');
+		await page.click('button.all');
+		await expect(page.locator('p')).toHaveText('layout: 4, page: 4');
+	});
+
+	test('invalidateAll persists through redirects', async ({ page }) => {
+		await page.goto('/load/invalidation/multiple/redirect');
+		await page.click('button.redirect');
+		await expect(page.locator('p.redirect-state')).toHaveText('Redirect state: done');
+	});
+
+	test('+layout(.server).js is re-run when server dep is invalidated', async ({ page }) => {
+		await page.goto('/load/invalidation/depends');
+		const server = await page.textContent('p.server');
+		const shared = await page.textContent('p.shared');
+		expect(server).toBeDefined();
+		expect(shared).toBeDefined();
+
+		await Promise.all([page.click('button.server'), page.waitForLoadState('networkidle')]);
+		await page.waitForTimeout(200);
+		const next_server = await page.textContent('p.server');
+		const next_shared = await page.textContent('p.shared');
+		expect(server).not.toBe(next_server);
+		expect(shared).not.toBe(next_shared);
+	});
+
+	test('+layout.js is re-run when shared dep is invalidated', async ({ page }) => {
+		await page.goto('/load/invalidation/depends');
+		const server = await page.textContent('p.server');
+		const shared = await page.textContent('p.shared');
+		expect(server).toBeDefined();
+		expect(shared).toBeDefined();
+
+		await Promise.all([page.click('button.shared'), page.waitForLoadState('networkidle')]);
+		await page.waitForTimeout(200);
+		const next_server = await page.textContent('p.server');
+		const next_shared = await page.textContent('p.shared');
+		expect(server).toBe(next_server);
+		expect(shared).not.toBe(next_shared);
+	});
+
+	test('Parameter use is tracked even for routes that do not use the parameters', async ({
+		page,
+		clicknav
+	}) => {
+		await page.goto('/load/invalidation/params');
+
+		await clicknav('[href="/load/invalidation/params/1"]');
+		expect(await page.textContent('pre')).toBe('{"a":"1"}');
+
+		await clicknav('[href="/load/invalidation/params/1/x"]');
+		expect(await page.textContent('pre')).toBe('{"a":"1","b":"x"}');
+
+		await page.goBack();
+		expect(await page.textContent('pre')).toBe('{"a":"1"}');
+	});
 });
 
 test.describe('data-sveltekit attributes', () => {
@@ -812,18 +904,9 @@ test('+server.js next to +page.svelte works', async ({ page }) => {
 	await page.goto('/routing/endpoint-next-to-page');
 	expect(await page.textContent('p')).toBe('Hi');
 
-	await page.click('button:has-text("GET")');
-	await expect(page.locator('pre')).toHaveText('GET');
-
-	await page.click('button:has-text("PUT")');
-	await expect(page.locator('pre')).toHaveText('PUT');
-
-	await page.click('button:has-text("PATCH")');
-	await expect(page.locator('pre')).toHaveText('PATCH');
-
-	await page.click('button:has-text("POST")');
-	await expect(page.locator('pre')).toHaveText('POST');
-
-	await page.click('button:has-text("DELETE")');
-	await expect(page.locator('pre')).toHaveText('DELETE');
+	for (const method of ['GET', 'PUT', 'PATCH', 'POST', 'DELETE']) {
+		page.click(`button:has-text("${method}")`);
+		await page.waitForResponse('/routing/endpoint-next-to-page');
+		expect(await page.textContent('pre')).toBe(method);
+	}
 });
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index 0404859e5..2ea3737df 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -1,5 +1,5 @@
 import { expect } from '@playwright/test';
-import { test } from '../../../utils.js';
+import { start_server, test } from '../../../utils.js';
 import { fetch } from 'undici';
 import { createHash, randomBytes } from 'node:crypto';
 
@@ -23,6 +23,29 @@ test.describe('Content-Type', () => {
 	});
 });
 
+test.describe('Cookies', () => {
+	test('does not forward cookies from external domains', async ({ request }) => {
+		const { close, port } = await start_server(async (req, res) => {
+			if (req.url === '/') {
+				res.writeHead(200, {
+					'set-cookie': 'external=true',
+					'access-control-allow-origin': '*'
+				});
+
+				res.end('ok');
+			} else {
+				res.writeHead(404);
+				res.end('not found');
+			}
+		});
+
+		const response = await request.get(`/load/fetch-external-no-cookies?port=${port}`);
+		expect(response.headers()['set-cookie']).not.toContain('external=true');
+
+		close();
+	});
+});
+
 test.describe('CSRF', () => {
 	test('Blocks requests with incorrect origin', async ({ baseURL }) => {
 		const res = await fetch(`${baseURL}/csrf`, {
@@ -249,6 +272,32 @@ test.describe('Load', () => {
 		const response = await request.get('/load/static-file-with-hash');
 		expect(await response.text()).toContain('status: 404');
 	});
+
+	test('includes origin header on non-GET internal request', async ({ page, baseURL }) => {
+		await page.goto('/load/fetch-origin-internal');
+		expect(await page.textContent('h1')).toBe(`origin: ${new URL(baseURL).origin}`);
+	});
+
+	test('includes origin header on external request', async ({ page, baseURL }) => {
+		const { port, close } = await start_server((req, res) => {
+			if (req.url === '/') {
+				res.writeHead(200, {
+					'content-type': 'application/json',
+					'access-control-allow-origin': '*'
+				});
+
+				res.end(JSON.stringify({ origin: req.headers.origin }));
+			} else {
+				res.writeHead(404);
+				res.end('not found');
+			}
+		});
+
+		await page.goto(`/load/fetch-origin-external?port=${port}`);
+		expect(await page.textContent('h1')).toBe(`origin: ${new URL(baseURL).origin}`);
+
+		close();
+	});
 });
 
 test.describe('Routing', () => {
@@ -336,6 +385,18 @@ test.describe('setHeaders', () => {
 	});
 });
 
+test.describe('cookies', () => {
+	test('cookie.serialize created correct cookie header string', async ({ page }) => {
+		const response = await page.goto('/cookies/serialize');
+		const cookies = await response.headerValue('set-cookie');
+		expect(
+			cookies.includes('before=before') &&
+				cookies.includes('after=after') &&
+				cookies.includes('endpoint=endpoint')
+		).toBe(true);
+	});
+});
+
 test.describe('Miscellaneous', () => {
 	test('does not serve version.json with an immutable cache header', async ({ request }) => {
 		// this isn't actually a great test, because caching behaviour is down to adapters.
diff --git a/packages/kit/test/apps/basics/test/setup.js b/packages/kit/test/apps/basics/test/setup.js
new file mode 100644
index 000000000..9be580053
--- /dev/null
+++ b/packages/kit/test/apps/basics/test/setup.js
@@ -0,0 +1,7 @@
+import fs from 'fs';
+
+if (process.platform !== 'win32') {
+	process.chdir('src/routes/routing');
+	fs.rmSync('symlink-from', { recursive: true, force: true });
+	fs.symlinkSync('symlink-to', 'symlink-from', 'dir');
+}
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 15c03f300..9332b0825 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1633,6 +1633,21 @@ test.describe('Routing', () => {
 		await page.goto('/static');
 		expect(await page.textContent('h1')).toBe('hello');
 	});
+
+	test('shows "Not Found" in 404 case', async ({ page }) => {
+		await page.goto('/404-fallback');
+		expect(await page.textContent('h1')).toBe('404');
+		expect(await page.textContent('p')).toBe('This is your custom error page saying: "Not Found"');
+	});
+
+	if (process.platform !== 'win32') {
+		test('Respects symlinks', async ({ page, clicknav }) => {
+			await page.goto('/routing');
+			await clicknav('[href="/routing/symlink-from"]');
+
+			expect(await page.textContent('h1')).toBe('symlinked');
+		});
+	}
 });
 
 test.describe('Matchers', () => {
@@ -1801,7 +1816,7 @@ test.describe('Actions', () => {
 		}
 	});
 
-	test('use:enhance', async ({ page, app }) => {
+	test('use:enhance', async ({ page }) => {
 		await page.goto('/actions/enhance');
 
 		expect(await page.textContent('pre')).toBe(JSON.stringify(null));
@@ -1845,6 +1860,17 @@ test.describe('Actions', () => {
 
 		await expect(page.locator('pre')).toHaveText(JSON.stringify({ result: 'register: foo' }));
 	});
+
+	test('redirect', async ({ page }) => {
+		await page.goto('/actions/redirect');
+
+		page.click('button');
+
+		await page.waitForResponse('/actions/redirect');
+		await page.waitForTimeout(50);
+
+		expect(page.url()).toContain('/actions/enhance');
+	});
 });
 
 test.describe('Cookies API', () => {
diff --git a/packages/kit/test/prerendering/basics/src/service-worker.js b/packages/kit/test/prerendering/basics/src/service-worker.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js
index 44d80bfff..a7135660a 100644
--- a/packages/kit/test/prerendering/basics/test/test.js
+++ b/packages/kit/test/prerendering/basics/test/test.js
@@ -225,4 +225,9 @@ test('prerenders a page in a (group)', () => {
 	assert.ok(content.includes('<h1>grouped</h1>'));
 });
 
+test('injects relative service worker', () => {
+	const content = read('index.html');
+	assert.ok(content.includes(`navigator.serviceWorker.register('./service-worker.js')`));
+});
+
 test.run();