Repository: sveltejs/kit (js)

Issue:
`invalidate('...')` doesn't work after navigating to a page with parameters
### Describe the bug

Repro: https://github.com/aradalvand/sveltekit-invalidate-with-params

It seems like having a route with params (e.g. `routes/whatever/[id]`) along with a load function that have `depends(...)` doesn't work as expected, when you navigate to that page with the client-side router, and then run `invalidate('...')`, it won't work. Very weird. I'm pretty confident this is a bug.

### Reproduction

- Clone https://github.com/aradalvand/sveltekit-invalidate-with-params
- Run and open the app (the home page) in the browser
- Open the console, run `invalidate('user')`. You'll see that the layout's load function gets re-run, as expected:
![image](https://user-images.githubusercontent.com/26527405/187087544-eee0938b-19a2-4e18-81aa-5beb0b12a9fb.png)
- Now, click on the link in the home page titled "Go to foo"
- After navigating to the `/foo/123` page, open the console and run the same command again. This time, it won't work, the layout's load function isn't re-run:
![image](https://user-images.githubusercontent.com/26527405/187087581-0f36b16e-2f3a-45b9-b4dc-01bcde127e62.png)
 

### Logs

_No response_

### System Info

```shell
System:
    OS: Linux 5.15 Ubuntu 20.04.4 LTS (Focal Fossa)
    CPU: (4) x64 Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz
    Memory: 486.11 MB / 3.77 GB
    Container: Yes
    Shell: 5.8 - /usr/bin/zsh
  Binaries:
    Node: 16.15.1 - /usr/bin/node
    npm: 8.11.0 - /usr/bin/npm
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.65
    @sveltejs/kit: next => 1.0.0-next.445
    svelte: ^3.44.0 => 3.49.0
    vite: ^3.0.4 => 3.0.9
```


### Severity

blocking all usage of SvelteKit

### Additional Information

The problem only exists for pages that have parameters, I have no idea why, but I think this information could be useful for diagnosing the problem.

Pull request description:
future-proof `invalidate()`
Closes #6354.

Related to #6489, #6274 and #5305 (we can remove the `breaking change` label from the latter two once this is merged).

It's very possible that we'd need to add an `options` object to `invalidate(...)` in future. Since overloaded functions are a terrible idea, that'd mean that to accommodate the no-argument case where you want to invalidate everything, we'd have to do this...

```js
invalidate(undefined, {...});
```

...which is sinful. A better approach, which happens to be clearer and easier to document, is to create a new `invalidateAll` function:

```diff
<script>
-  import { invalidate } from '$app/navigation';
+  import { invalidateAll } from '$app/navigation';
</script>

-<button on:click={() => invalidate()}>reload</button>
+<button on:click={() => invalidateAll()}>reload</button>
```

Separately, I think it's probably more useful if the predicate form takes a `URL` instead of a string:

```diff
-invalidate((dep) => dep.includes('/some/matched/pathname'));
+invalidate((url) => url.pathname.startsWith('/some/matched/pathname'));
```

Unfortunately this second part is the sort of breaking change that's basically impossible to add a runtime error for. If people think a string (i.e. `url.href`) is preferable, then we could keep it as it is.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/cold-penguins-share.md b/.changeset/cold-penguins-share.md
new file mode 100644
index 000000000..0e267cbde
--- /dev/null
+++ b/.changeset/cold-penguins-share.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/adapter-netlify': patch
+---
+
+Update adapter to only glob files
diff --git a/.changeset/ninety-tables-switch.md b/.changeset/ninety-tables-switch.md
new file mode 100644
index 000000000..37bd6b1f8
--- /dev/null
+++ b/.changeset/ninety-tables-switch.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] call `invalidate(fn)` predicates with a URL instead of a string
diff --git a/.changeset/odd-months-grow.md b/.changeset/odd-months-grow.md
new file mode 100644
index 000000000..d8bd476bd
--- /dev/null
+++ b/.changeset/odd-months-grow.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Prefer JSON responses when returning errors if accept header is `*/*`
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 4925a20ee..96e4bf9f8 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -159,6 +159,7 @@
     "cold-llamas-brake",
     "cold-panthers-learn",
     "cold-pants-unite",
+    "cold-penguins-share",
     "cold-sheep-yawn",
     "cold-tables-shake",
     "cool-bananas-share",
@@ -704,6 +705,7 @@
     "orange-boats-love",
     "orange-ties-tell",
     "orange-wolves-fly",
+    "perfect-cycles-smell",
     "perfect-ears-explain",
     "perfect-experts-hammer",
     "perfect-frogs-decide",
diff --git a/.changeset/rude-nails-work.md b/.changeset/rude-nails-work.md
new file mode 100644
index 000000000..e11a00cbf
--- /dev/null
+++ b/.changeset/rude-nails-work.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] replace invalidate() with invalidateAll()
diff --git a/.changeset/shiny-vans-jam.md b/.changeset/shiny-vans-jam.md
new file mode 100644
index 000000000..84664ebdb
--- /dev/null
+++ b/.changeset/shiny-vans-jam.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Use `invalidateAll()`
diff --git a/documentation/docs/05-load.md b/documentation/docs/05-load.md
index eb83c9111..0aaf5389d 100644
--- a/documentation/docs/05-load.md
+++ b/documentation/docs/05-load.md
@@ -99,12 +99,14 @@ An instance of [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL), co
 
 #### depends
 
-This function declares that the `load` function has a _dependency_ on one or more URLs, which can subsequently be used with [`invalidate()`](/docs/modules#$app-navigation-invalidate) to cause `load` to rerun.
+This function declares that the `load` function has a _dependency_ on one or more URLs or custom identifiers, which can subsequently be used with [`invalidate()`](/docs/modules#$app-navigation-invalidate) to cause `load` to rerun.
 
 Most of the time you won't need this, as `fetch` calls `depends` on your behalf — it's only necessary if you're using a custom API client that bypasses `fetch`.
 
 URLs can be absolute or relative to the page being loaded, and must be [encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding).
 
+Custom identifiers have to be prefixed with one or more lowercase letters followed by a colon to conform to the [URI specification](https://www.rfc-editor.org/rfc/rfc3986.html)
+
 ```js
 // @filename: ambient.d.ts
 declare module '$lib/api' {
@@ -121,7 +123,11 @@ import * as api from '$lib/api';
 
 /** @type {import('./$types').PageLoad} */
 export async function load({ depends }) {
-	depends(`${api.base}/foo`, `${api.base}/bar`);
+	depends(
+		`${api.base}/foo`,
+		`${api.base}/bar`,
+		'my-stuff:foo'
+	);
 
 	return {
 		foo: api.client.get('/foo'),
diff --git a/packages/adapter-auto/CHANGELOG.md b/packages/adapter-auto/CHANGELOG.md
index 4874f83b9..3f061b1de 100644
--- a/packages/adapter-auto/CHANGELOG.md
+++ b/packages/adapter-auto/CHANGELOG.md
@@ -1,5 +1,13 @@
 # @sveltejs/adapter-auto
 
+## 1.0.0-next.71
+
+### Patch Changes
+
+- Updated dependencies [[`6b06da84`](https://github.com/sveltejs/kit/commit/6b06da84b2454f42cbf7e8bcb643169af74428db)]:
+  - @sveltejs/adapter-netlify@1.0.0-next.76
+  - @sveltejs/adapter-vercel@1.0.0-next.72
+
 ## 1.0.0-next.70
 
 ### Patch Changes
diff --git a/packages/adapter-auto/package.json b/packages/adapter-auto/package.json
index 5d6b60b96..69258ebbf 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.70",
+	"version": "1.0.0-next.71",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/adapter-netlify/CHANGELOG.md b/packages/adapter-netlify/CHANGELOG.md
index 32ccc7abe..52eadbe17 100644
--- a/packages/adapter-netlify/CHANGELOG.md
+++ b/packages/adapter-netlify/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/adapter-netlify
 
+## 1.0.0-next.76
+
+### Patch Changes
+
+- Update adapter to only glob files ([#6492](https://github.com/sveltejs/kit/pull/6492))
+
 ## 1.0.0-next.75
 
 ### Patch Changes
diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js
index 4528b8a3c..cb99dfab4 100644
--- a/packages/adapter-netlify/index.js
+++ b/packages/adapter-netlify/index.js
@@ -163,7 +163,7 @@ async function generate_lambda_functions({ builder, publish, split, esm }) {
 	if (esm) {
 		builder.copy(`${files}/esm`, '.netlify', { replace });
 	} else {
-		glob('**/*.js', { cwd: '.netlify/server' }).forEach((file) => {
+		glob('**/*.js', { cwd: '.netlify/server', filesOnly: true }).forEach((file) => {
 			const filepath = `.netlify/server/${file}`;
 			const input = readFileSync(filepath, 'utf8');
 			const output = esbuild.transformSync(input, { format: 'cjs', target: 'node12' }).code;
diff --git a/packages/adapter-netlify/package.json b/packages/adapter-netlify/package.json
index d7abd110f..b459484f6 100644
--- a/packages/adapter-netlify/package.json
+++ b/packages/adapter-netlify/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/adapter-netlify",
-	"version": "1.0.0-next.75",
+	"version": "1.0.0-next.76",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/create-svelte/templates/default/src/lib/form.ts b/packages/create-svelte/templates/default/src/lib/form.ts
index c38dee6ec..6723c612f 100644
--- a/packages/create-svelte/templates/default/src/lib/form.ts
+++ b/packages/create-svelte/templates/default/src/lib/form.ts
@@ -1,4 +1,4 @@
-import { invalidate } from '$app/navigation';
+import { invalidateAll } from '$app/navigation';
 
 // this action (https://svelte.dev/tutorial/actions) allows us to
 // progressively enhance a <form> that already works without JS
@@ -83,7 +83,7 @@ export function enhance(
 
 			if (response.ok) {
 				if (result) result({ data, form, response });
-				invalidate();
+				invalidateAll();
 			} else if (error) {
 				error({ data, form, error: null, response });
 			} else {
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 293d22723..8c86a5604 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/kit
 
+## 1.0.0-next.460
+
+### Patch Changes
+
+- [breaking] Remove sveltekit:start event ([#6484](https://github.com/sveltejs/kit/pull/6484))
+
 ## 1.0.0-next.459
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index f2792ef4d..4a84a24be 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.459",
+	"version": "1.0.0-next.460",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js
index d97e6dea6..974e83a13 100644
--- a/packages/kit/src/runtime/app/navigation.js
+++ b/packages/kit/src/runtime/app/navigation.js
@@ -16,6 +16,7 @@ export const disableScrollHandling = ssr
 	: client.disable_scroll_handling;
 export const goto = ssr ? guard('goto') : client.goto;
 export const invalidate = ssr ? guard('invalidate') : client.invalidate;
+export const invalidateAll = ssr ? guard('invalidateAll') : client.invalidateAll;
 export const prefetch = ssr ? guard('prefetch') : client.prefetch;
 export const prefetchRoutes = ssr ? guard('prefetchRoutes') : client.prefetch_routes;
 export const beforeNavigate = ssr ? () => {} : client.before_navigate;
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 6b95fbbfd..190348e84 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -70,7 +70,7 @@ function check_for_removed_attributes() {
  * @returns {import('./types').Client}
  */
 export function create_client({ target, base, trailing_slash }) {
-	/** @type {Array<((href: string) => boolean)>} */
+	/** @type {Array<((url: URL) => boolean)>} */
 	const invalidated = [];
 
 	/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
@@ -103,6 +103,7 @@ export function create_client({ target, base, trailing_slash }) {
 
 	/** @type {Promise<void> | null} */
 	let invalidating = null;
+	let force_invalidation = false;
 
 	/** @type {import('svelte').SvelteComponent} */
 	let root;
@@ -139,6 +140,19 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {{}} */
 	let token;
 
+	function invalidate() {
+		if (!invalidating) {
+			invalidating = Promise.resolve().then(async () => {
+				await update(new URL(location.href), []);
+
+				invalidating = null;
+				force_invalidation = false;
+			});
+		}
+
+		return invalidating;
+	}
+
 	/**
 	 * @param {string | URL} url
 	 * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
@@ -639,6 +653,8 @@ export function create_client({ target, base, trailing_slash }) {
 	 * @param {{ url: boolean, params: string[] }} changed
 	 */
 	function has_changed(changed, parent_changed, uses) {
+		if (force_invalidation) return true;
+
 		if (!uses) return false;
 
 		if (uses.parent && parent_changed) return true;
@@ -648,8 +664,8 @@ export function create_client({ target, base, trailing_slash }) {
 			if (uses.params.has(param)) return true;
 		}
 
-		for (const dep of uses.dependencies) {
-			if (invalidated.some((fn) => fn(dep))) return true;
+		for (const href of uses.dependencies) {
+			if (invalidated.some((fn) => fn(new URL(href)))) return true;
 		}
 
 		return false;
@@ -1057,28 +1073,25 @@ export function create_client({ target, base, trailing_slash }) {
 
 		invalidate: (resource) => {
 			if (resource === undefined) {
-				// Force rerun of all load functions, regardless of their dependencies
-				for (const node of current.branch) {
-					node?.server?.uses.dependencies.add('');
-					node?.shared?.uses.dependencies.add('');
-				}
-				invalidated.push(() => true);
-			} else if (typeof resource === 'function') {
+				// TODO remove for 1.0
+				throw new Error(
+					'`invalidate()` (with no arguments) has been replaced by `invalidateAll()`'
+				);
+			}
+
+			if (typeof resource === 'function') {
 				invalidated.push(resource);
 			} else {
 				const { href } = new URL(resource, location.href);
-				invalidated.push((dep) => dep === href);
+				invalidated.push((url) => url.href === href);
 			}
 
-			if (!invalidating) {
-				invalidating = Promise.resolve().then(async () => {
-					await update(new URL(location.href), []);
-
-					invalidating = null;
-				});
-			}
+			return invalidate();
+		},
 
-			return invalidating;
+		invalidateAll: () => {
+			force_invalidation = true;
+			return invalidate();
 		},
 
 		prefetch: async (href) => {
diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts
index e86bc3ae0..f2f5a3617 100644
--- a/packages/kit/src/runtime/client/types.d.ts
+++ b/packages/kit/src/runtime/client/types.d.ts
@@ -3,6 +3,7 @@ import {
 	beforeNavigate,
 	goto,
 	invalidate,
+	invalidateAll,
 	prefetch,
 	prefetchRoutes
 } from '$app/navigation';
@@ -17,6 +18,7 @@ export interface Client {
 	disable_scroll_handling: () => void;
 	goto: typeof goto;
 	invalidate: typeof invalidate;
+	invalidateAll: typeof invalidateAll;
 	prefetch: typeof prefetch;
 	prefetch_routes: typeof prefetchRoutes;
 
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index d4659cdbe..437de80e5 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -194,8 +194,8 @@ export function handle_fatal_error(event, options, error) {
 
 	// ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it
 	const type = negotiate(event.request.headers.get('accept') || 'text/html', [
-		'text/html',
-		'application/json'
+		'application/json',
+		'text/html'
 	]);
 
 	if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 15d742150..0dca0aa9f 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -113,10 +113,25 @@ declare module '$app/navigation' {
 		opts?: { replaceState?: boolean; noscroll?: boolean; keepfocus?: boolean; state?: any }
 	): Promise<void>;
 	/**
-	 * Causes any `load` functions belonging to the currently active page to re-run if they `fetch` the resource in question. If no argument is given, all resources will be invalidated. Returns a `Promise` that resolves when the page is subsequently updated.
-	 * @param dependency The invalidated resource
+	 * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated.
+	 *
+	 * If the argument is given as a `string` or `URL`, it must resolve to the same URL that was passed to `fetch` or `depends` (including query parameters).
+	 * To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL.
+	 *
+	 * The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned.
+	 * This can be useful if you want to invalidate based on a pattern instead of a exact match.
+	 *
+	 * ```ts
+	 * // Example: Match '/path' regardless of the query parameters
+	 * invalidate((url) => url.pathname === '/path');
+	 * ```
+	 * @param url The invalidated URL
+	 */
+	export function invalidate(url: string | URL | ((url: URL) => boolean)): Promise<void>;
+	/**
+	 * Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
 	 */
-	export function invalidate(dependency?: string | ((href: string) => boolean)): Promise<void>;
+	export function invalidateAll(): Promise<void>;
 	/**
 	 * Programmatically prefetches the given page, which means
 	 *  1. ensuring that the code for the page is loaded, and


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte
index 9acf3f221..11134eaf5 100644
--- a/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/change-detection/one/[x]/+page.svelte
@@ -1,5 +1,5 @@
 <script>
-	import { invalidate } from '$app/navigation';
+	import { invalidate, invalidateAll } from '$app/navigation';
 
 	/** @type {import('./$types').PageData} */
 	export let data;
@@ -10,7 +10,7 @@
 <button
 	on:click={async () => {
 		window.invalidated = false;
-		await invalidate((dep) => dep.includes('change-detection/data.json'));
+		await invalidate((url) => url.pathname.includes('change-detection/data.json'));
 		window.invalidated = true;
 	}}>invalidate change-detection/data.json</button
 >
@@ -18,7 +18,7 @@
 <button
 	on:click={async () => {
 		window.invalidated = false;
-		await invalidate();
+		await invalidateAll();
 		window.invalidated = true;
 	}}>invalidate all</button
 >
diff --git a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte
index 0f544b39f..b66f248de 100644
--- a/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/invalidation/forced/+page.svelte
@@ -1,5 +1,5 @@
 <script>
-	import { invalidate } from '$app/navigation';
+	import { invalidateAll } from '$app/navigation';
 
 	/** @type {import('./$types').PageData} */
 	export let data;
@@ -7,4 +7,4 @@
 
 <h1>a: {data.a}, b: {data.b}</h1>
 
-<button on:click={() => invalidate()}>invalidate</button>
+<button on:click={invalidateAll}>invalidate</button>
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index c409a799e..f22bcd6a4 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -149,15 +149,39 @@ test.describe('Errors', () => {
 	});
 
 	test('throw error(...) in endpoint', async ({ request, read_errors }) => {
-		const res = await request.get('/errors/endpoint-throw-error');
+		// HTML
+		{
+			const res = await request.get('/errors/endpoint-throw-error', {
+				headers: {
+					accept: 'text/html'
+				}
+			});
+
+			const error = read_errors('/errors/endpoint-throw-error');
+			expect(error).toBe(undefined);
+
+			expect(res.status()).toBe(401);
+			expect(await res.text()).toContain(
+				'This is the static error page with the following message: You shall not pass'
+			);
+		}
 
-		const error = read_errors('/errors/endpoint-throw-error');
-		expect(error).toBe(undefined);
+		// JSON (default)
+		{
+			const res = await request.get('/errors/endpoint-throw-error');
 
-		expect(await res?.text()).toContain(
-			'This is the static error page with the following message: You shall not pass'
-		);
-		expect(res?.status()).toBe(401);
+			const error = read_errors('/errors/endpoint-throw-error');
+			expect(error).toBe(undefined);
+
+			expect(res.status()).toBe(401);
+			expect(await res.json()).toEqual({
+				status: 401,
+				message: 'You shall not pass',
+
+				// TODO this is gross, fix it
+				__is_http_error: true
+			});
+		}
 	});
 
 	test('throw redirect(...) in endpoint', async ({ page, read_errors }) => {
@@ -170,13 +194,38 @@ test.describe('Errors', () => {
 		expect(await page.textContent('h1')).toBe('the answer is 42');
 	});
 
-	test('error thrown in handle results in a rendered error page', async ({ request }) => {
-		const res = await request.get('/errors/error-in-handle');
+	test('error thrown in handle results in a rendered error page or JSON response', async ({
+		request
+	}) => {
+		// HTML
+		{
+			const res = await request.get('/errors/error-in-handle', {
+				headers: {
+					accept: 'text/html'
+				}
+			});
+
+			expect(res.status()).toBe(500);
+			expect(await res.text()).toContain(
+				'This is the static error page with the following message: Error in handle'
+			);
+		}
+
+		// JSON (default)
+		{
+			const res = await request.get('/errors/error-in-handle');
 
-		expect(await res.text()).toContain(
-			'This is the static error page with the following message: Error in handle'
-		);
-		expect(res.status()).toBe(500);
+			const error = await res.json();
+
+			expect(typeof error.stack).toBe('string');
+			delete error.stack;
+
+			expect(res.status()).toBe(500);
+			expect(error).toEqual({
+				name: 'Error',
+				message: 'Error in handle'
+			});
+		}
 	});
 });