Repository: sveltejs/kit (JavaScript)

Issue:
Cookies are not set after subsequent page reloads
### Describe the bug

Initially discovered by @shahriar350 in #7055

The `handle` and `load` functions do not set a new cookie if it is deleted in the browser.

This is proven by deleting the cookie and reloading the page. The cookies are not set after subsequent reloads.

However, invalidating the page and reloading causes the cookies to be set successfully.

Unfortunately, when running `npm run preview`, only the `handle` cookie gets set this way. The `load` cookie does not get set even after invalidating.

### Reproduction

https://github.com/s3812497/sveltekit-cookie-bug

1. Run the dev server.
2. Open the project in the browser.
3. Delete the cookies.
4. Reload the page.

Expected:
Cookies are set again.

Actual:
Cookies are not set.

### Logs

_No response_

### System Info

```shell
System:
    OS: macOS 13.0
    CPU: (8) arm64 Apple M1 Pro
    Memory: 793.89 MB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.17.0 - ~/.nvm/versions/node/v16.17.0/bin/node
    npm: 8.15.0 - ~/.nvm/versions/node/v16.17.0/bin/npm
  Browsers:
    Chrome: 105.0.5195.125
    Firefox: 105.0.1
    Firefox Developer Edition: 105.0
    Safari: 16.1
    Safari Technology Preview: 16.0
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.80 
    @sveltejs/kit: next => 1.0.0-next.504 
    svelte: ^3.50.1 => 3.50.1 
    vite: ^3.1.4 => 3.1.4
```


### Severity

serious, but I can work around it

### Additional Information

_No response_


Discussion:
Ok, I went down this rabbit hole, and emerged 🐇 with the following conclusions:

Messing with server-side cookies -- even reading them  -- on the client is not something that should be encouraged. Safari's `document.cookie` does not seem to delete cookies. They are deleted in the "storage" tab in Safari's dev tools, and not sent on subsequent requests, but parsing `document.cookie` with client-side javascript shows deleted cookies, even across page reloads. 

Repro: https://github.com/cdcarson/sveltekit-cookie-bug (Based on the OP's repro. Try it in Safari.)

So, relying on on a cookie value in the client seems like a bad idea. A worse idea is allowing client-side javascript to modify or delete cookies originally set on the server.

Suggestions:

1. The existing `cookies.set`, and `cookies.delete` should be `httpOnly`, no exceptions.
2.  `cookies.set` should sign the cookie. `cookies.get` should verify. This will negate the risk of the browser replacing or pre-populating a cookie key that is meant to be secure.
3.  Add `cookies.getInsecure(name: string)` to allow the server to read cookies set on the client. Make it clear that this should be used for `theme=dark` stuff, not anything that has security implications.





> A worse idea is allowing client-side javascript to modify or delete cookies originally set on the server.

In the original discussion, the cookie was being deleted through the browser dev tools. Would this issue still persist if the cookies are httpOnly?
> A worse idea is allowing client-side javascript to modify or delete cookies originally set on the server.

But what about a theme cookie that is usually set on the client but the client has js disabled. So you call the server to set the clients cookie. There are many more use cases for cookies with http only disabled and i don't think it's a thing sveltekit should handle. You have full control about the cookies, it's up to the user to decide how to use it.

> Add cookies.getInsecure(name: string) to allow the server to read cookies set on the client.

Not 100% sure but I don't think the cookie header includes the cookie options, only name and value. So there is no way to distinguish between http only on or off
> > A worse idea is allowing client-side javascript to modify or delete cookies originally set on the server.
> 
> But what about a theme cookie that is usually set on the client but the client has js disabled. So you call the server to set the clients cookie. There are many more use cases for cookies with http only disabled and i don't think it's a thing sveltekit should handle. You have full control about the cookies, it's up to the user to decide how to use it.
> 
> > Add cookies.getInsecure(name: string) to allow the server to read cookies set on the client.
> 
> Not 100% sure but I don't think the cookie header includes the cookie options, only name and value. So there is no way to distinguish between http only on or off

i tried with httponly cookie and expire time was set to 10s. after 10s cookie is removed by the browser but the result is the same. A cookie is empty, svelte response header add the cookie but never set the cookie to browser unless i remove cache and hard reload the page(chrome, edge in windows).
> In the original discussion, the cookie was being deleted through the browser dev tools. Would this issue still persist if the cookies are httpOnly?

@s3812497 You could still delete the cookies via dev tools. 

To clarify my comment: Playing with your repro that you posted for this issue, I noticed weird differences between Chrome and Safari. It became apparent that we can't use the client to accurately parse what server cookies have been set. So for example [in your repro this](https://github.com/s3812497/sveltekit-cookie-bug/blob/0238357e425bcb7ed591d2c5905afd4d1fc95892/src/routes/%2Bpage.svelte#L27-L28)...
```html
<h2>document.cookie:</h2>
<p>{cookies ? JSON.stringify(cookies) : 'No cookies were set. This is a bug.'}</p>
```
...isn't really reliable. It's really hard to point out exactly why, especially since browsers handle `document.cookie` differently.

Net: I wasn't able to reproduce the original issue in Chrome. I was able to do so, apparently, in Safari.
@david-plugge

> Not 100% sure but I don't think the cookie header includes the cookie options

You are correct. It's just a string of key-value pairs. Thus signing the cookies. That's the only way of ensuring the cookie was set on the server.

> But what about a theme cookie that is usually set on the client but the client has js disabled.

We could have `setInsecureCookie`? Or say that setting the theme is just a progressive feature. Or have both js and non-js clients hit an endpoint, rather than setting it on the client.

> You have full control about the cookies, it's up to the user to decide how to use it.

I agree that should be as true as possible, but since SvelteKit has a cookies api (for a good reason -- i.e. reading & writing cookies across separate `load`s in the same request) some parameters have to be set. 

My suggestion that cookies be `HttpOnly` stemmed from playing with the repro in this issue, and realizing that reading `document.cookie` was flaky across different browsers. So I'm not even sure that the repro here is accurate.




> Net: I wasn't able to reproduce the original issue in Chrome. I was able to do so, apparently, in Safari.

Is it possible that `Disable cache` is enabled in your Chrome? I had to turn this off to see the effects.

Regardless, I'm really thankful for your detailed findings :) I'm learning a lot. Initially, I wasn't sure what the expected behaviour is for this.
> Is it possible that Disable cache is enabled in your Chrome? 

Oh, probably. Yikes. I'll try it again later this afternoon. 
I can reproduce this in Chrome when turning off `Disable cache`. The culprit seems to be the etag logic. The browser sends a matching etag so SvelteKit says "alright let's respond with 304 instead" and leaves out the cookies. I'm not sure if the fix is to append the cookies, or not send 304 when cookies changed, or if the etag generation logic is wrong somewhere else.
my error is i can make any other requests to the server but when i reload and the client side makes a request again it will not include the cookies with the request and my server side needs cookie in every request to authorize and i dont now what to do
> my error is i can make any other requests to the server but when i reload and the client side makes a request again it will not include the cookies with the request and my server side needs cookie in every request to authorize and i dont now what to do

Can you open a new issue for this and include a reproduction?
> my error is i can make any other requests to the server but when i reload and the client side makes a request again it will not include the cookies with the request and my server side needs cookie in every request to authorize and i dont now what to do


Use credentials: 'include' in every request where cookies are needed; it will automatically send them to the server.



Patch:
diff --git a/.changeset/gorgeous-months-shave.md b/.changeset/gorgeous-months-shave.md
new file mode 100644
index 000000000000..7690c201afbe
--- /dev/null
+++ b/.changeset/gorgeous-months-shave.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] forward cookie headers on etag response
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index 8c1ec9d1f5f3..ff51bf21df1a 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -327,8 +327,15 @@ export async function respond(request, options, state) {
 			if (if_none_match_value === etag) {
 				const headers = new Headers({ etag });
 
-				// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
-				for (const key of ['cache-control', 'content-location', 'date', 'expires', 'vary']) {
+				// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie
+				for (const key of [
+					'cache-control',
+					'content-location',
+					'date',
+					'expires',
+					'vary',
+					'set-cookie'
+				]) {
 					const value = response.headers.get(key);
 					if (value) headers.set(key, value);
 				}


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag/+page.server.js b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag/+page.server.js
new file mode 100644
index 000000000000..689539c43d9f
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag/+page.server.js
@@ -0,0 +1,4 @@
+/** @type {import('./$types').PageServerLoad} */
+export function load({ cookies }) {
+	cookies.set('foo', 'bar', { httpOnly: false });
+}
diff --git a/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag/+page.svelte b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag/+page.svelte
new file mode 100644
index 000000000000..64ec825cd6db
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag/+page.svelte
@@ -0,0 +1,20 @@
+<script>
+	import { onMount } from 'svelte';
+
+	let cookies;
+
+	onMount(() => {
+		cookies = document.cookie;
+	});
+</script>
+
+<button
+	on:click={() => {
+		document.cookie = 'foo=bar;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
+		location.reload();
+	}}
+>
+	Delete cookies and reload the page
+</button>
+
+<p>{cookies}</p>
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index b847d6dfdcf8..1e34c648f9e7 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -930,3 +930,12 @@ test.describe('Content negotiation', () => {
 		await expect(page.locator('[data-testid="form-result"]')).toHaveText('form.submitted: true');
 	});
 });
+
+test.describe('cookies', () => {
+	test('etag forwards cookies', async ({ page }) => {
+		await page.goto('/cookies/forwarded-in-etag');
+		await expect(page.locator('p')).toHaveText('foo=bar');
+		await Promise.all([page.waitForNavigation(), page.click('button')]);
+		await expect(page.locator('p')).toHaveText('foo=bar');
+	});
+});