Repository: sveltejs/kit (js)

Issue:
Cannot set set-cookie response headers multiple times
### Describe the bug

Since the `setHeaders` takes a POJO and also checks that a response header as not been already set, one cannot set multiple cookies with multiple `set-cookie` headers. 

### Reproduction

Source code for `setHeaders` checks whether an header is already been set, so can’t even call it multiple times.

### Logs

_No response_

### System Info

```shell
Any system
```


### Severity

serious, but I can work around it

### Additional Information

_No response_

Pull request:
[fix] handle set-cookie in setHeaders
Fixes #6032

### 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/fresh-pigs-tease.md b/.changeset/fresh-pigs-tease.md
new file mode 100644
index 000000000..9af327627
--- /dev/null
+++ b/.changeset/fresh-pigs-tease.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] remove ability for `+page.server.js` to respond to `GET` requests with JSON
diff --git a/.changeset/honest-parrots-compare.md b/.changeset/honest-parrots-compare.md
new file mode 100644
index 000000000..4c8442e71
--- /dev/null
+++ b/.changeset/honest-parrots-compare.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Handle `throw error/redirect` in `+server.js`
diff --git a/.changeset/lemon-kids-double.md b/.changeset/lemon-kids-double.md
new file mode 100644
index 000000000..e98b9d4d2
--- /dev/null
+++ b/.changeset/lemon-kids-double.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+handle `set-cookie` in `setHeaders`
diff --git a/.changeset/mighty-students-develop.md b/.changeset/mighty-students-develop.md
new file mode 100644
index 000000000..cbffb0dea
--- /dev/null
+++ b/.changeset/mighty-students-develop.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Handle windows paths and regexp chars in kit.alias
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 58258eebe..ea7fe6e55 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -256,6 +256,7 @@
     "few-buses-argue",
     "few-carrots-sell",
     "few-cobras-switch",
+    "few-hornets-joke",
     "few-points-taste",
     "few-rockets-sort",
     "few-rockets-think",
@@ -335,6 +336,7 @@
     "gold-carrots-sniff",
     "gold-colts-train",
     "gold-moles-hunt",
+    "gold-rocks-hang",
     "gold-spies-fail",
     "gold-trains-dream",
     "good-beans-invite",
diff --git a/.changeset/selfish-tips-divide.md b/.changeset/selfish-tips-divide.md
new file mode 100644
index 000000000..ac7e76767
--- /dev/null
+++ b/.changeset/selfish-tips-divide.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Run svelte-kit sync before svelte-check in check scripts
diff --git a/packages/create-svelte/CHANGELOG.md b/packages/create-svelte/CHANGELOG.md
index dbdb840a5..f03c4fa56 100644
--- a/packages/create-svelte/CHANGELOG.md
+++ b/packages/create-svelte/CHANGELOG.md
@@ -1,5 +1,11 @@
 # create-svelte
 
+## 2.0.0-next.156
+
+### Patch Changes
+
+- update `app.d.ts` for library skeleton template ([#6020](https://github.com/sveltejs/kit/pull/6020))
+
 ## 2.0.0-next.155
 
 ### Patch Changes
diff --git a/packages/create-svelte/package.json b/packages/create-svelte/package.json
index ca8f89dce..56e941bcc 100644
--- a/packages/create-svelte/package.json
+++ b/packages/create-svelte/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "create-svelte",
-	"version": "2.0.0-next.155",
+	"version": "2.0.0-next.156",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/create-svelte/shared/+typescript/package.json b/packages/create-svelte/shared/+typescript/package.json
index 465f674b8..7b5612f97 100644
--- a/packages/create-svelte/shared/+typescript/package.json
+++ b/packages/create-svelte/shared/+typescript/package.json
@@ -1,7 +1,7 @@
 {
 	"scripts": {
-		"check": "svelte-check --tsconfig ./tsconfig.json",
-		"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch"
+		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
 	},
 	"devDependencies": {
 		"typescript": "^4.7.4",
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 7b859cf14..fdef39d3d 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/kit
 
+## 1.0.0-next.421
+
+### Patch Changes
+
+- Respect `export const prerender = false` in `+page.server.js` ([#6012](https://github.com/sveltejs/kit/pull/6012))
+
 ## 1.0.0-next.420
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 664069a66..3645e8c6a 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.420",
+	"version": "1.0.0-next.421",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index 338791b4d..9d112214a 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -1,3 +1,4 @@
+import { HttpError, Redirect } from '../../index/private.js';
 import { check_method_names, method_not_allowed } from './utils.js';
 
 /**
@@ -29,16 +30,29 @@ export async function render_endpoint(event, route) {
 		return method_not_allowed(mod, method);
 	}
 
-	const response = await handler(
-		/** @type {import('types').RequestEvent<Record<string, any>>} */ (event)
-	);
-
-	if (!(response instanceof Response)) {
-		return new Response(
-			`Invalid response from route ${event.url.pathname}: handler should return a Response object`,
-			{ status: 500 }
+	try {
+		const response = await handler(
+			/** @type {import('types').RequestEvent<Record<string, any>>} */ (event)
 		);
-	}
 
-	return response;
+		if (!(response instanceof Response)) {
+			return new Response(
+				`Invalid response from route ${event.url.pathname}: handler should return a Response object`,
+				{ status: 500 }
+			);
+		}
+
+		return response;
+	} catch (error) {
+		if (error instanceof HttpError) {
+			return new Response(error.message, { status: error.status });
+		} else if (error instanceof Redirect) {
+			return new Response(undefined, {
+				status: error.status,
+				headers: { Location: error.location }
+			});
+		} else {
+			throw error;
+		}
+	}
 }
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 625fc0539..61d4033a8 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -118,6 +118,9 @@ export async function respond(request, options, state) {
 	/** @type {import('types').ResponseHeaders} */
 	const headers = {};
 
+	/** @type {string[]} */
+	const cookies = [];
+
 	/** @type {import('types').RequestEvent} */
 	const event = {
 		get clientAddress() {
@@ -141,16 +144,26 @@ export async function respond(request, options, state) {
 		setHeaders: (new_headers) => {
 			for (const key in new_headers) {
 				const lower = key.toLowerCase();
+				const value = new_headers[key];
 
-				if (lower in headers) {
-					throw new Error(`"${key}" header is already set`);
-				}
+				if (lower === 'set-cookie') {
+					const new_cookies = /** @type {string[]} */ (Array.isArray(value) ? value : [value]);
 
-				// TODO apply these headers to the response
-				headers[lower] = new_headers[key];
+					for (const cookie of new_cookies) {
+						if (cookies.includes(cookie)) {
+							throw new Error(`"${key}" header already has cookie with same value`);
+						}
 
-				if (state.prerendering && lower === 'cache-control') {
-					state.prerendering.cache = /** @type {string} */ (new_headers[key]);
+						cookies.push(cookie);
+					}
+				} else if (lower in headers) {
+					throw new Error(`"${key}" header is already set`);
+				} else {
+					headers[lower] = value;
+
+					if (state.prerendering && lower === 'cache-control') {
+						state.prerendering.cache = /** @type {string} */ (value);
+					}
 				}
 			}
 		},
@@ -312,19 +325,19 @@ export async function respond(request, options, state) {
 								: await render_page(event, route, options, state, resolve_opts);
 					}
 
-					for (const key in headers) {
-						const value = headers[key];
-						if (key === 'set-cookie') {
-							for (const cookie of Array.isArray(value) ? value : [value]) {
-								response.headers.append(key, /** @type {string} */ (cookie));
-							}
-						} else if (!is_data_request) {
-							// we only want to set cookies on __data.json requests, we don't
-							// want to cache stuff erroneously etc
+					if (!is_data_request) {
+						// we only want to set cookies on __data.json 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));
 						}
 					}
 
+					for (const cookie of cookies) {
+						response.headers.append('set-cookie', cookie);
+					}
+
 					// respond with 304 if etag matches
 					if (response.status === 200 && response.headers.has('etag')) {
 						let if_none_match_value = request.headers.get('if-none-match');
diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js
index edaa42016..91f0a6470 100644
--- a/packages/kit/src/runtime/server/page/index.js
+++ b/packages/kit/src/runtime/server/page/index.js
@@ -36,7 +36,11 @@ export async function render_page(event, route, options, state, resolve_opts) {
 		'application/json'
 	]);
 
-	if (accept === 'application/json') {
+	if (
+		accept === 'application/json' &&
+		event.request.method !== 'GET' &&
+		event.request.method !== 'HEAD'
+	) {
 		const node = await options.manifest._.nodes[route.leaf]();
 		if (node.server) {
 			return handle_json_request(event, options, node.server);
@@ -346,8 +350,8 @@ function get_page_config(leaf, options) {
  * @param {import('types').SSRNode['server']} mod
  */
 export async function handle_json_request(event, options, mod) {
-	const method = /** @type {import('types').HttpMethod} */ (event.request.method);
-	const handler = mod[method === 'HEAD' || method === 'GET' ? 'load' : method];
+	const method = /** @type {'POST' | 'PUT' | 'PATCH' | 'DELETE'} */ (event.request.method);
+	const handler = mod[method];
 
 	if (!handler) {
 		return method_not_allowed(mod, method);
@@ -357,14 +361,6 @@ export async function handle_json_request(event, options, mod) {
 		// @ts-ignore
 		const result = await handler.call(null, event);
 
-		if (method === 'HEAD') {
-			return new Response();
-		}
-
-		if (method === 'GET') {
-			return json(result);
-		}
-
 		if (result?.errors) {
 			// @ts-ignore
 			return json({ errors: result.errors }, { status: result.status || 400 });
diff --git a/packages/kit/src/vite/utils.js b/packages/kit/src/vite/utils.js
index 6c62d2636..4e9ae7543 100644
--- a/packages/kit/src/vite/utils.js
+++ b/packages/kit/src/vite/utils.js
@@ -2,6 +2,7 @@ import fs from 'fs';
 import path from 'path';
 import { loadConfigFromFile, loadEnv, normalizePath } from 'vite';
 import { runtime_directory } from '../core/utils.js';
+import { posixify } from '../utils/filesystem.js';
 
 /**
  * @param {import('vite').ResolvedConfig} config
@@ -113,18 +114,22 @@ export function get_aliases(config) {
 	];
 
 	for (let [key, value] of Object.entries(config.alias)) {
+		value = posixify(value);
 		if (value.endsWith('/*')) {
 			value = value.slice(0, -2);
 		}
 		if (key.endsWith('/*')) {
 			// Doing just `{ find: key.slice(0, -2) ,..}` would mean `import .. from "key"` would also be matched, which we don't want
 			alias.push({
-				find: new RegExp(`^${key.slice(0, -2)}\\/(.+)$`),
+				find: new RegExp(`^${escape_for_regexp(key.slice(0, -2))}\\/(.+)$`),
 				replacement: `${path.resolve(value)}/$1`
 			});
 		} else if (key + '/*' in config.alias) {
 			// key and key/* both exist -> the replacement for key needs to happen _only_ on import .. from "key"
-			alias.push({ find: new RegExp(`^${key}$`), replacement: path.resolve(value) });
+			alias.push({
+				find: new RegExp(`^${escape_for_regexp(key)}$`),
+				replacement: path.resolve(value)
+			});
 		} else {
 			alias.push({ find: key, replacement: path.resolve(value) });
 		}
@@ -133,6 +138,13 @@ export function get_aliases(config) {
 	return alias;
 }
 
+/**
+ * @param {string} str
+ */
+function escape_for_regexp(str) {
+	return str.replace(/[.*+?^${}()|[\]\\]/g, (match) => '\\' + match);
+}
+
 /**
  * Given an entry point like [cwd]/src/hooks, returns a filename like [cwd]/src/hooks.js or [cwd]/src/hooks/index.js
  * @param {string} entry
diff --git a/packages/kit/src/vite/utils.spec.js b/packages/kit/src/vite/utils.spec.js
index a566434f5..6fa338f64 100644
--- a/packages/kit/src/vite/utils.spec.js
+++ b/packages/kit/src/vite/utils.spec.js
@@ -207,7 +207,15 @@ test('merge resolve.alias', () => {
 
 test('transform kit.alias to resolve.alias', () => {
 	const config = validate_config({
-		kit: { alias: { simpleKey: 'simple/value', key: 'value', 'key/*': 'value/*' } }
+		kit: {
+			alias: {
+				simpleKey: 'simple/value',
+				key: 'value',
+				'key/*': 'value/*',
+				$regexChar: 'windows\\path',
+				'$regexChar/*': 'windows\\path\\*'
+			}
+		}
 	});
 
 	const transformed = get_aliases(config.kit).map((entry) => {
@@ -227,7 +235,9 @@ test('transform kit.alias to resolve.alias', () => {
 		{ find: '$lib', replacement: 'src/lib' },
 		{ find: 'simpleKey', replacement: 'simple/value' },
 		{ find: /^key$/.toString(), replacement: 'value' },
-		{ find: /^key\/(.+)$/.toString(), replacement: 'value/$1' }
+		{ find: /^key\/(.+)$/.toString(), replacement: 'value/$1' },
+		{ find: /^\$regexChar$/.toString(), replacement: 'windows/path' },
+		{ find: /^\$regexChar\/(.+)$/.toString(), replacement: 'windows/path/$1' }
 	]);
 });
 


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/errors/endpoint-throw-error/+server.js b/packages/kit/test/apps/basics/src/routes/errors/endpoint-throw-error/+server.js
new file mode 100644
index 000000000..14c9aec1c
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/errors/endpoint-throw-error/+server.js
@@ -0,0 +1,5 @@
+import { error } from '@sveltejs/kit';
+
+export function GET() {
+	throw error(401, 'You shall not pass');
+}
diff --git a/packages/kit/test/apps/basics/src/routes/errors/endpoint-throw-redirect/+server.js b/packages/kit/test/apps/basics/src/routes/errors/endpoint-throw-redirect/+server.js
new file mode 100644
index 000000000..26300742e
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/errors/endpoint-throw-redirect/+server.js
@@ -0,0 +1,5 @@
+import { redirect } from '@sveltejs/kit';
+
+export function GET() {
+	throw redirect(302, '/');
+}
diff --git a/packages/kit/test/apps/basics/src/routes/headers/set-cookie/+layout.server.js b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/+layout.server.js
new file mode 100644
index 000000000..21fd24121
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/+layout.server.js
@@ -0,0 +1,5 @@
+export function load({ setHeaders }) {
+	setHeaders({
+		'set-cookie': 'cookie1=value1'
+	});
+}
diff --git a/packages/kit/test/apps/basics/src/routes/headers/set-cookie/+layout.svelte b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/+layout.svelte
new file mode 100644
index 000000000..0385342ce
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/+layout.svelte
@@ -0,0 +1,1 @@
+<slot />
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/src/routes/headers/set-cookie/sub/+page.server.js b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/sub/+page.server.js
new file mode 100644
index 000000000..1d7acff07
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/sub/+page.server.js
@@ -0,0 +1,5 @@
+export function load({ setHeaders }) {
+	setHeaders({
+		'set-cookie': 'cookie2=value2'
+	});
+}
diff --git a/packages/kit/test/apps/basics/src/routes/headers/set-cookie/sub/+page.svelte b/packages/kit/test/apps/basics/src/routes/headers/set-cookie/sub/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js
index 45615bcb5..1cafac3ba 100644
--- a/packages/kit/test/apps/basics/test/server.test.js
+++ b/packages/kit/test/apps/basics/test/server.test.js
@@ -124,40 +124,6 @@ test.describe('Errors', () => {
 		expect(await response.text()).toMatch('thisvariableisnotdefined is not defined');
 	});
 
-	test('page endpoint thrown error respects `accept: application/json`', async ({ request }) => {
-		const response = await request.get('/errors/page-endpoint/get-implicit', {
-			headers: {
-				accept: 'application/json'
-			}
-		});
-
-		const { message, name, stack, fancy } = await response.json();
-
-		expect(response.status()).toBe(500);
-		expect(name).toBe('FancyError');
-		expect(message).toBe('oops');
-		expect(fancy).toBe(true);
-
-		if (process.env.DEV) {
-			expect(stack.split('\n').length).toBeGreaterThan(1);
-		} else {
-			expect(stack.split('\n').length).toBe(1);
-		}
-	});
-
-	test('page endpoint returned error respects `accept: application/json`', async ({ request }) => {
-		const response = await request.get('/errors/page-endpoint/get-explicit', {
-			headers: {
-				accept: 'application/json'
-			}
-		});
-
-		const { message } = await response.json();
-
-		expect(response.status()).toBe(400);
-		expect(message).toBe('oops');
-	});
-
 	test('returns 400 when accessing a malformed URI', async ({ page }) => {
 		const response = await page.goto('/%c0%ae%c0%ae/etc/passwd');
 		if (process.env.DEV) {
@@ -181,6 +147,26 @@ test.describe('Errors', () => {
 			'This is your custom error page saying: "Cannot read properties of undefined (reading \'toUpperCase\')"'
 		);
 	});
+
+	test('throw error(..) in endpoint', async ({ page, read_errors }) => {
+		const res = await page.goto('/errors/endpoint-throw-error');
+
+		const error = read_errors('/errors/endpoint-throw-error');
+		expect(error).toBe(undefined);
+
+		expect(await res?.text()).toBe('You shall not pass');
+		expect(res?.status()).toBe(401);
+	});
+
+	test('throw redirect(..) in endpoint', async ({ page, read_errors }) => {
+		const res = await page.goto('/errors/endpoint-throw-redirect');
+		expect(res?.status()).toBe(200); // redirects are opaque to the browser
+
+		const error = read_errors('/errors/endpoint-throw-redirect');
+		expect(error).toBe(undefined);
+
+		expect(await page.textContent('h1')).toBe('the answer is 42');
+	});
 });
 
 test.describe('Load', () => {
@@ -211,53 +197,6 @@ test.describe('Routing', () => {
 });
 
 test.describe('Shadowed pages', () => {
-	test('responds to HEAD requests from endpoint', async ({ request }) => {
-		const url = '/shadowed/simple';
-
-		const opts = {
-			headers: {
-				accept: 'application/json'
-			}
-		};
-
-		const responses = {
-			head: await request.head(url, opts),
-			get: await request.get(url, opts)
-		};
-
-		const headers = {
-			head: responses.head.headers(),
-			get: responses.get.headers()
-		};
-
-		expect(responses.head.status()).toBe(200);
-		expect(responses.get.status()).toBe(200);
-		expect(await responses.head.text()).toBe('');
-		expect(await responses.get.json()).toEqual({ answer: 42 });
-
-		['date', 'transfer-encoding'].forEach((name) => {
-			delete headers.head[name];
-			delete headers.get[name];
-		});
-
-		expect(headers.get).toEqual({
-			...headers.head,
-			'content-type': 'application/json'
-		});
-	});
-
-	test('Responds from endpoint if Accept includes application/json but not text/html', async ({
-		request
-	}) => {
-		const response = await request.get('/shadowed/simple', {
-			headers: {
-				accept: 'application/json'
-			}
-		});
-
-		expect(await response.json()).toEqual({ answer: 42 });
-	});
-
 	test('Action can return undefined', async ({ request }) => {
 		const response = await request.post('/shadowed/simple/post', {
 			headers: {
@@ -314,6 +253,14 @@ test.describe('Static files', () => {
 	});
 });
 
+test.describe('setHeaders', () => {
+	test('allows multiple set-cookie headers with different values', async ({ page }) => {
+		const response = await page.goto('/headers/set-cookie/sub');
+		const cookies = (await response?.allHeaders())['set-cookie'];
+		expect(cookies.includes('cookie1=value1') && cookies.includes('cookie2=value2')).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.