Repository: sveltejs/kit (js)

Issue:
Add a `cookies` interface to `RequestEvent` and `LoadServerEvent`
### Describe the problem

In #5748, we introduced the `setHeaders` API:

```js
export function load({ setHeaders }) {
  setHeaders({
    'set-cookie': 'answer=42'
  });
}
```

[Recent explorations](https://github.com/sveltejs/kit/discussions/5875#discussioncomment-3540263) (see the 'cookie handling' section in particular) suggest the API falls short. We need a way to set a cookie inside an action that can be read during the subsequent `load`, and while we _could_ make it work with `setHeaders` it would involve some very unexpected behaviour (we would basically need to intercept `request.headers.get('cookie')` and sub in a value that included the `set-cookie`, which is a bad amount of magic).

### Describe the proposed solution

We add a new `cookies` interface:

```js
// src/routes/login/+page.server.js
export const actions = {
  default: ({ fields, cookies }) => {
    const user = await db.getUserFromEmail(fields.get('email');

    // ...some logic happens...

    cookies.set('sessionid', await db.createSession(user), {
        maxAge: 30 * 24 * 60 * 60 * 1000 // stay logged in for 30 days
    });
  }
};
```

> `export const actions` is described in https://github.com/sveltejs/kit/discussions/5875#discussioncomment-3540263

```js
// src/routes/+layout.server.js
export async function load({ cookies }) {
  // this value could come from `request.headers.get('cookie')` or the previous `cookies.set` call
  const sessionid = cookies.get('sessionid');
  const user = await db.getUserFromSessionId(sessionid);
  return { user };
}
```

There is an experimental standard [`CookieStore`](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore) interface, but I don't think we should use it. It's designed for use in browsers, which means all the methods are asynchronous — needlessly, in our case — and there's a `change` event that doesn't really make sense in our context. More important, it doesn't expose an `httpOnly` option (because that wouldn't make sense for client-side JavaScript), whereas I think `httpOnly` is the only sensible default.

The proposed API:

```ts
interface Cookies {
  delete(name: string): void;

  get(name: string, opts: {
    decode: (input: string) => string
  }): string;

  set(name: string, value: string, opts?: {
    expires?: Date;
    maxAge?: number;
    domain?: string;
    path?: string;
    secure?: boolean;
    httpOnly: boolean;
    sameSite: 'strict' | 'lax' | 'none'; // can only be `none` with `secure: true`,
    encode: (input: string) => string
  }); void;
}
```

If you're familiar with the [cookie](https://www.npmjs.com/package/cookie) package, this API will seem very familiar — it's what we'd use under the hood.

### Alternatives considered

Relying on people populating `event.locals` in actions, which feels unnecessarily restrictive.

Intercepting (and changing) `request.headers.get('cookie')`, which feels like too much magic.

### Importance

would make my life easier

### Additional Information

_No response_

Pull request description:
add new cookies API
closes #6540

* [x] docs
* [x] update tests to read cookies with this API instead of from request headers

### 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/grumpy-jobs-poke.md b/.changeset/grumpy-jobs-poke.md
new file mode 100644
index 000000000..4fb84562a
--- /dev/null
+++ b/.changeset/grumpy-jobs-poke.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] add API for interacting with cookies
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 34c767e9b..b666b9f93 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -31,7 +31,12 @@
     "test-writes": "0.0.2-next.0",
     "svelte-migrate": "1.0.0-next.0",
     "skeleton-template": "0.0.1-next.0",
-    "@sveltejs/package": "1.0.0-next.1"
+    "@sveltejs/package": "1.0.0-next.1",
+    "private-dynamic-env": "0.0.1",
+    "private-dynamic-env-dynamic-import": "0.0.1",
+    "private-static-env": "0.0.1",
+    "private-static-env-dynamic-import": "0.0.1",
+    "server-only-module": "0.0.1"
   },
   "changesets": [
     "afraid-eels-tease",
@@ -723,6 +728,7 @@
     "perfect-ghosts-shake",
     "perfect-kiwis-sneeze",
     "perfect-mayflies-sneeze",
+    "perfect-rules-burn",
     "perfect-socks-sparkle",
     "pink-ducks-press",
     "pink-flies-mix",
@@ -924,6 +930,7 @@
     "shy-oranges-sin",
     "shy-toes-punch",
     "silent-crews-change",
+    "silent-cycles-reply",
     "silly-ants-battle",
     "silly-grapes-cover",
     "silly-jokes-hug",
diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md
index 92f6f270e..2dc453bd2 100644
--- a/documentation/docs/03-routing.md
+++ b/documentation/docs/03-routing.md
@@ -141,7 +141,7 @@ export {};
 import { error } from '@sveltejs/kit';
 
 /** @type {import('./$types').Action} */
-export async function POST({ request, setHeaders, url }) {
+export async function POST({ cookies, request, url }) {
 	const values = await request.formData();
 
 	const username = /** @type {string} */ (values.get('username'));
@@ -167,8 +167,8 @@ export async function POST({ request, setHeaders, url }) {
 		};
 	}
 
-	setHeaders({
-		'set-cookie': createSessionCookie(user.id)
+	cookies.set('sessionid', createSessionCookie(user.id), {
+		httpOnly: true
 	});
 
 	return {
diff --git a/documentation/docs/05-load.md b/documentation/docs/05-load.md
index 6da83a4c2..8671ac310 100644
--- a/documentation/docs/05-load.md
+++ b/documentation/docs/05-load.md
@@ -18,7 +18,7 @@ export function load(event) {
 
 ### Input properties
 
-The argument to a `load` function is a `LoadEvent` (or, for server-only `load` functions, a `ServerLoadEvent` which inherits `clientAddress`, `locals`, `platform` and `request` from `RequestEvent`). All events have the following properties:
+The argument to a `load` function is a `LoadEvent` (or, for server-only `load` functions, a `ServerLoadEvent` which inherits `clientAddress`, `cookies`, `locals`, `platform` and `request` from `RequestEvent`). All events have the following properties:
 
 #### data
 
@@ -221,6 +221,7 @@ export async function load({ parent, fetch }) {
 If you need to set headers for the response, you can do so using the `setHeaders` method. This is useful if you want the page to be cached, for example:
 
 ```js
+// @errors: 2322
 /// file: src/routes/blog/+page.js
 /** @type {import('./$types').PageLoad} */
 export async function load({ fetch, setHeaders }) {
@@ -240,25 +241,7 @@ export async function load({ fetch, setHeaders }) {
 
 Setting the same header multiple times (even in separate `load` functions) is an error — you can only set a given header once.
 
-The exception is `set-cookie`, which can be set multiple times and can be passed an array of strings:
-
-```js
-/// file: src/routes/+layout.server.js
-/** @type {import('./$types').LayoutLoad} */
-export async function load({ setHeaders }) {
-	setHeaders({
-		'set-cookie': 'a=1; HttpOnly'
-	});
-
-	setHeaders({
-		'set-cookie': 'b=2; HttpOnly'
-	});
-
-	setHeaders({
-		'set-cookie': ['c=3; HttpOnly', 'd=4; HttpOnly']
-	});
-}
-```
+You cannot add a `set-cookie` header with `setHeaders` — use the [`cookies`](/docs/types#sveltejs-kit-cookies) API in a server-only `load` function instead.
 
 ### Output
 
diff --git a/documentation/docs/06-hooks.md b/documentation/docs/06-hooks.md
index 8992b92ae..0a5effb21 100644
--- a/documentation/docs/06-hooks.md
+++ b/documentation/docs/06-hooks.md
@@ -40,7 +40,7 @@ declare namespace App {
 	}
 }
 
-const getUserInformation: (cookie: string | null) => Promise<User>;
+const getUserInformation: (cookie: string | undefined) => Promise<User>;
 
 // declare global {
 // 	const getUserInformation: (cookie: string) => Promise<User>;
@@ -50,7 +50,7 @@ const getUserInformation: (cookie: string | null) => Promise<User>;
 // ---cut---
 /** @type {import('@sveltejs/kit').Handle} */
 export async function handle({ event, resolve }) {
-	event.locals.user = await getUserInformation(event.request.headers.get('cookie'));
+	event.locals.user = await getUserInformation(event.cookies.get('sessionid'));
 
 	const response = await resolve(event);
 	response.headers.set('x-custom-header', 'potato');
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 7d837b9b7..192935101 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,13 @@
 # @sveltejs/kit
 
+## 1.0.0-next.471
+
+### Patch Changes
+
+- [breaking] Allow users to designate modules as server-only ([#6422](https://github.com/sveltejs/kit/pull/6422))
+
+* [breaking] exclude headers from serialized responses by default, add `filterSerializedResponseHeaders` `resolve` option ([#6569](https://github.com/sveltejs/kit/pull/6569))
+
 ## 1.0.0-next.470
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index ec15e374e..ec78c15da 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.470",
+	"version": "1.0.0-next.471",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js
new file mode 100644
index 000000000..abb31ee91
--- /dev/null
+++ b/packages/kit/src/runtime/server/cookie.js
@@ -0,0 +1,76 @@
+import * as cookie from 'cookie';
+
+/**
+ * @param {Request} request
+ * @param {URL} url
+ */
+export function get_cookies(request, url) {
+	const initial_cookies = cookie.parse(request.headers.get('cookie') ?? '');
+
+	/** @type {Array<{ name: string, value: string, options: import('cookie').CookieSerializeOptions }>} */
+	const new_cookies = [];
+
+	/** @type {import('types').Cookies} */
+	const cookies = {
+		get(name, opts) {
+			const decode = opts?.decode || decodeURIComponent;
+
+			let i = new_cookies.length;
+			while (i--) {
+				const cookie = new_cookies[i];
+
+				if (
+					cookie.name === name &&
+					domain_matches(url.hostname, cookie.options.domain) &&
+					path_matches(url.pathname, cookie.options.path)
+				) {
+					return cookie.value;
+				}
+			}
+
+			return name in initial_cookies ? decode(initial_cookies[name]) : undefined;
+		},
+		set(name, value, options = {}) {
+			new_cookies.push({
+				name,
+				value,
+				options: {
+					httpOnly: true,
+					secure: true,
+					...options
+				}
+			});
+		},
+		delete(name) {
+			new_cookies.push({ name, value: '', options: { expires: new Date(0) } });
+		}
+	};
+
+	return { cookies, new_cookies };
+}
+
+/**
+ * @param {string} hostname
+ * @param {string} [constraint]
+ */
+export function domain_matches(hostname, constraint) {
+	if (!constraint) return true;
+
+	const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint;
+
+	if (hostname === normalized) return true;
+	return hostname.endsWith('.' + normalized);
+}
+
+/**
+ * @param {string} path
+ * @param {string} [constraint]
+ */
+export function path_matches(path, constraint) {
+	if (!constraint) return true;
+
+	const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint;
+
+	if (path === normalized) return true;
+	return path.startsWith(normalized + '/');
+}
diff --git a/packages/kit/src/runtime/server/page/cookie.spec.js b/packages/kit/src/runtime/server/cookie.spec.js
similarity index 100%
rename from packages/kit/src/runtime/server/page/cookie.spec.js
rename to packages/kit/src/runtime/server/cookie.spec.js
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 45acb2e47..36e6051b7 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -1,3 +1,4 @@
+import * as cookie from 'cookie';
 import { render_endpoint } from './endpoint.js';
 import { render_page } from './page/index.js';
 import { render_response } from './page/render.js';
@@ -8,6 +9,7 @@ import { decode_params, disable_search, normalize_path } from '../../utils/url.j
 import { exec } from '../../utils/routing.js';
 import { render_data } from './data/index.js';
 import { DATA_SUFFIX } from '../../constants.js';
+import { get_cookies } from './cookie.js';
 
 /* global __SVELTEKIT_ADAPTER_NAME__ */
 
@@ -116,16 +118,16 @@ export async function respond(request, options, state) {
 		}
 	}
 
-	/** @type {import('types').ResponseHeaders} */
+	/** @type {Record<string, string>} */
 	const headers = {};
 
-	/** @type {string[]} */
-	const cookies = [];
+	const { cookies, new_cookies } = get_cookies(request, url);
 
 	if (state.prerendering) disable_search(url);
 
 	/** @type {import('types').RequestEvent} */
 	const event = {
+		cookies,
 		getClientAddress:
 			state.getClientAddress ||
 			(() => {
@@ -144,15 +146,9 @@ export async function respond(request, options, state) {
 				const value = new_headers[key];
 
 				if (lower === 'set-cookie') {
-					const new_cookies = /** @type {string[]} */ (Array.isArray(value) ? value : [value]);
-
-					for (const cookie of new_cookies) {
-						if (cookies.includes(cookie)) {
-							throw new Error(`"${key}" header already has cookie with same value`);
-						}
-
-						cookies.push(cookie);
-					}
+					throw new Error(
+						`Use \`event.cookie.set(name, value, options)\` instead of \`event.setHeaders\` to set cookies`
+					);
 				} else if (lower in headers) {
 					throw new Error(`"${key}" header is already set`);
 				} else {
@@ -275,8 +271,11 @@ export async function respond(request, options, state) {
 					}
 				}
 
-				for (const cookie of cookies) {
-					response.headers.append('set-cookie', cookie);
+				for (const new_cookie of new_cookies) {
+					response.headers.append(
+						'set-cookie',
+						cookie.serialize(new_cookie.name, new_cookie.value, new_cookie.options)
+					);
 				}
 
 				// respond with 304 if etag matches
@@ -338,6 +337,14 @@ export async function respond(request, options, state) {
 		} catch (e) {
 			const error = coalesce_to_error(e);
 			return handle_fatal_error(event, options, error);
+		} finally {
+			event.cookies.set = () => {
+				throw new Error('Cannot use `cookies.set(...)` after the response has been generated');
+			};
+
+			event.setHeaders = () => {
+				throw new Error('Cannot use `setHeaders(...)` after the response has been generated');
+			};
 		}
 	}
 
diff --git a/packages/kit/src/runtime/server/page/cookie.js b/packages/kit/src/runtime/server/page/cookie.js
deleted file mode 100644
index 25d1cb219..000000000
--- a/packages/kit/src/runtime/server/page/cookie.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * @param {string} hostname
- * @param {string} [constraint]
- */
-export function domain_matches(hostname, constraint) {
-	if (!constraint) return true;
-
-	const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint;
-
-	if (hostname === normalized) return true;
-	return hostname.endsWith('.' + normalized);
-}
-
-/**
- * @param {string} path
- * @param {string} [constraint]
- */
-export function path_matches(path, constraint) {
-	if (!constraint) return true;
-
-	const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint;
-
-	if (path === normalized) return true;
-	return path.startsWith(normalized + '/');
-}
diff --git a/packages/kit/src/runtime/server/page/fetch.js b/packages/kit/src/runtime/server/page/fetch.js
index 525d6f755..87c034da9 100644
--- a/packages/kit/src/runtime/server/page/fetch.js
+++ b/packages/kit/src/runtime/server/page/fetch.js
@@ -1,7 +1,7 @@
 import * as cookie from 'cookie';
 import * as set_cookie_parser from 'set-cookie-parser';
 import { respond } from '../index.js';
-import { domain_matches, path_matches } from './cookie.js';
+import { domain_matches, path_matches } from '../cookie.js';
 
 /**
  * @param {{
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index f6d0e4ba8..e580c3549 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -12,7 +12,6 @@ import {
 	Prerendered,
 	PrerenderOnErrorValue,
 	RequestOptions,
-	ResponseHeaders,
 	RouteDefinition,
 	TrailingSlash
 } from './private.js';
@@ -119,6 +118,27 @@ export interface Config {
 	[key: string]: any;
 }
 
+export interface Cookies {
+	/**
+	 * Gets a cookie that was previously set with `cookies.set`, or from the request headers.
+	 */
+	get(name: string, opts?: import('cookie').CookieParseOptions): string | undefined;
+
+	/**
+	 * Sets a cookie. This will add a `set-cookie` header to the response, but also make
+	 * the cookie available via `cookies.get` during the current request.
+	 *
+	 * 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
+	 */
+	set(name: string, value: string, opts?: import('cookie').CookieSerializeOptions): void;
+
+	/**
+	 * Deletes a cookie by setting its value to an empty string and setting the expiry date in the past.
+	 */
+	delete(name: string): void;
+}
+
 export interface KitConfig {
 	adapter?: Adapter;
 	alias?: Record<string, string>;
@@ -213,7 +233,7 @@ export interface LoadEvent<
 	params: Params;
 	data: Data;
 	routeId: string | null;
-	setHeaders: (headers: ResponseHeaders) => void;
+	setHeaders: (headers: Record<string, string>) => void;
 	url: URL;
 	parent: () => Promise<ParentData>;
 	depends: (...deps: string[]) => void;
@@ -250,13 +270,14 @@ export interface ParamMatcher {
 export interface RequestEvent<
 	Params extends Partial<Record<string, string>> = Partial<Record<string, string>>
 > {
+	cookies: Cookies;
 	getClientAddress: () => string;
 	locals: App.Locals;
 	params: Params;
 	platform: Readonly<App.Platform>;
 	request: Request;
 	routeId: string | null;
-	setHeaders: (headers: ResponseHeaders) => void;
+	setHeaders: (headers: Record<string, string>) => void;
 	url: URL;
 }
 
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index 12fe1323a..24b587ddc 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -193,9 +193,6 @@ export interface RequestOptions {
 	platform?: App.Platform;
 }
 
-/** `string[]` is only for set-cookie, everything else must be type of `string` */
-export type ResponseHeaders = Record<string, string | number | string[] | null>;
-
 export interface RouteDefinition {
 	id: string;
 	type: 'page' | 'endpoint';


Test patch:
diff --git a/packages/kit/test/apps/basics/src/hooks.js b/packages/kit/test/apps/basics/src/hooks.js
index 4cb664e2c..71885efa2 100644
--- a/packages/kit/test/apps/basics/src/hooks.js
+++ b/packages/kit/test/apps/basics/src/hooks.js
@@ -1,5 +1,4 @@
 import fs from 'fs';
-import cookie from 'cookie';
 import { sequence } from '@sveltejs/kit/hooks';
 
 /** @type {import('@sveltejs/kit').HandleError} */
@@ -23,8 +22,7 @@ export const handle = sequence(
 		return resolve(event);
 	},
 	({ event, resolve }) => {
-		const cookies = cookie.parse(event.request.headers.get('cookie') || '');
-		event.locals.name = cookies.name;
+		event.locals.name = event.cookies.get('name');
 		return resolve(event);
 	},
 	async ({ event, resolve }) => {
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
index 21fd24121..1c73347b4 100644
--- 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
@@ -1,5 +1,6 @@
-export function load({ setHeaders }) {
-	setHeaders({
-		'set-cookie': 'cookie1=value1'
+/** @type {import('./$types').LayoutServerLoad} */
+export function load({ cookies }) {
+	cookies.set('cookie1', 'value1', {
+		secure: false // safari
 	});
 }
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
index 1d7acff07..ef3b8838b 100644
--- 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
@@ -1,5 +1,6 @@
-export function load({ setHeaders }) {
-	setHeaders({
-		'set-cookie': 'cookie2=value2'
+/** @type {import('./$types').PageServerLoad} */
+export function load({ cookies }) {
+	cookies.set('cookie2', 'value2', {
+		secure: false // safari
 	});
 }
diff --git a/packages/kit/test/apps/basics/src/routes/load/set-cookie-fetch/b.json/+server.js b/packages/kit/test/apps/basics/src/routes/load/set-cookie-fetch/b.json/+server.js
index 15c8f34f3..a8c4cd790 100644
--- a/packages/kit/test/apps/basics/src/routes/load/set-cookie-fetch/b.json/+server.js
+++ b/packages/kit/test/apps/basics/src/routes/load/set-cookie-fetch/b.json/+server.js
@@ -1,11 +1,8 @@
 import { json } from '@sveltejs/kit';
 
 /** @type {import('./$types').RequestHandler} */
-export function GET({ request }) {
-	const cookie = request.headers.get('cookie');
-
-	const match = /answer=([^;]+)/.exec(cookie);
-	const answer = +match?.[1];
+export function GET({ cookies }) {
+	const answer = +cookies.get('answer');
 
 	return json(
 		{ answer },
diff --git a/packages/kit/test/apps/basics/src/routes/shadowed/redirect-get-with-cookie/+page.server.js b/packages/kit/test/apps/basics/src/routes/shadowed/redirect-get-with-cookie/+page.server.js
index 2f479b88b..77fe8c5c7 100644
--- a/packages/kit/test/apps/basics/src/routes/shadowed/redirect-get-with-cookie/+page.server.js
+++ b/packages/kit/test/apps/basics/src/routes/shadowed/redirect-get-with-cookie/+page.server.js
@@ -1,6 +1,9 @@
 import { redirect } from '@sveltejs/kit';
 
-export function load({ setHeaders }) {
-	setHeaders({ 'set-cookie': 'shadow-redirect=happy' });
+/** @type {import('./$types').PageServerLoad} */
+export function load({ cookies }) {
+	cookies.set('shadow-redirect', 'happy', {
+		secure: false // safari
+	});
 	throw redirect(302, '/shadowed/redirected');
 }
diff --git a/packages/kit/test/apps/basics/src/routes/shadowed/redirect-post-with-cookie/+page.server.js b/packages/kit/test/apps/basics/src/routes/shadowed/redirect-post-with-cookie/+page.server.js
index bdd7b53b5..36e6b0e7a 100644
--- a/packages/kit/test/apps/basics/src/routes/shadowed/redirect-post-with-cookie/+page.server.js
+++ b/packages/kit/test/apps/basics/src/routes/shadowed/redirect-post-with-cookie/+page.server.js
@@ -1,6 +1,9 @@
 import { redirect } from '@sveltejs/kit';
 
-export function POST({ setHeaders }) {
-	setHeaders({ 'set-cookie': 'shadow-redirect=happy' });
+/** @type {import('./$types').PageServerLoad} */
+export function POST({ cookies }) {
+	cookies.set('shadow-redirect', 'happy', {
+		secure: false // safari
+	});
 	throw redirect(302, '/shadowed/redirected');
 }