Skip to content

Commit 52a674e

Browse files
committed
fix: support multiple cookies with the same name across different paths
1 parent 1164ffa commit 52a674e

File tree

5 files changed

+74
-17
lines changed

5 files changed

+74
-17
lines changed

.changeset/shiny-spoons-stick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: support multiple cookies with the same name across different paths

packages/kit/src/exports/public.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,13 @@ export interface Cookies {
212212
* Gets a cookie that was previously set with `cookies.set`, or from the request headers.
213213
* @param name the name of the cookie
214214
* @param opts the options, passed directly to `cookie.parse`. See documentation [here](https://github.com/jshttp/cookie#cookieparsestr-options)
215+
* @param target the target, used to determine the domain and path of the cookie.
215216
*/
216-
get: (name: string, opts?: import('cookie').CookieParseOptions) => string | undefined;
217+
get: (
218+
name: string,
219+
opts?: import('cookie').CookieParseOptions,
220+
target?: { domain?: string; path?: string }
221+
) => string | undefined;
217222

218223
/**
219224
* Gets all cookies that were previously set with `cookies.set`, or from the request headers.

packages/kit/src/runtime/server/cookie.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ function validate_options(options) {
2626
}
2727
}
2828

29+
/**
30+
* Generates a unique key for a cookie based on its domain, path, and name in
31+
* cookies[<domain>/<path>?<name>].
32+
* If the domain or path is undefined, it will be omitted.
33+
* For example, cookies[/?name], cookies['example.com/foo?name'].
34+
*
35+
* @param {string | undefined} domain
36+
* @param {string} path
37+
* @param {string} name
38+
*/
39+
function generate_cookie_key(domain, path, name) {
40+
return `${domain || ''}${path}?${name}`;
41+
}
42+
2943
/**
3044
* @param {Request} request
3145
* @param {URL} url
@@ -57,9 +71,11 @@ export function get_cookies(request, url) {
5771
/**
5872
* @param {string} name
5973
* @param {import('cookie').CookieParseOptions} [opts]
74+
* @param {{domain?: string, path?: string}} [target]
6075
*/
61-
get(name, opts) {
62-
const c = new_cookies[name];
76+
get(name, opts, target) {
77+
const cookie_key = generate_cookie_key(target?.domain, target?.path || url?.pathname, name);
78+
const c = new_cookies[cookie_key];
6379
if (
6480
c &&
6581
domain_matches(url.hostname, c.options.domain) &&
@@ -213,10 +229,18 @@ export function get_cookies(request, url) {
213229
path = resolve(normalized_url, path);
214230
}
215231

216-
new_cookies[name] = { name, value, options: { ...options, path } };
232+
new_cookies[generate_cookie_key(options.domain, path, name)] = {
233+
name,
234+
value,
235+
options: { ...options, path }
236+
};
217237

218238
if (__SVELTEKIT_DEV__) {
219-
const serialized = serialize(name, value, new_cookies[name].options);
239+
const serialized = serialize(
240+
name,
241+
value,
242+
new_cookies[generate_cookie_key(options.domain, path, name)].options
243+
);
220244
if (new TextEncoder().encode(serialized).byteLength > MAX_COOKIE_SIZE) {
221245
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
222246
}

packages/kit/src/runtime/server/cookie.spec.js

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ test('a cookie should not be present after it is deleted', () => {
6969
test('default values when set is called', () => {
7070
const { cookies, new_cookies } = cookies_setup();
7171
cookies.set('a', 'b', { path: '/' });
72-
const opts = new_cookies['a']?.options;
72+
const opts = new_cookies['/?a']?.options;
7373
assert.equal(opts?.secure, true);
7474
assert.equal(opts?.httpOnly, true);
7575
assert.equal(opts?.path, '/');
@@ -79,7 +79,7 @@ test('default values when set is called', () => {
7979
test('default values when set is called on sub path', () => {
8080
const { cookies, new_cookies } = cookies_setup({ href: 'https://example.com/foo/bar' });
8181
cookies.set('a', 'b', { path: '' });
82-
const opts = new_cookies['a']?.options;
82+
const opts = new_cookies['/foo/bar?a']?.options;
8383
assert.equal(opts?.secure, true);
8484
assert.equal(opts?.httpOnly, true);
8585
assert.equal(opts?.path, '/foo/bar');
@@ -89,14 +89,14 @@ test('default values when set is called on sub path', () => {
8989
test('default values when on localhost', () => {
9090
const { cookies, new_cookies } = cookies_setup({ href: 'http://localhost:1234' });
9191
cookies.set('a', 'b', { path: '/' });
92-
const opts = new_cookies['a']?.options;
92+
const opts = new_cookies['/?a']?.options;
9393
assert.equal(opts?.secure, false);
9494
});
9595

9696
test('overridden defaults when set is called', () => {
9797
const { cookies, new_cookies } = cookies_setup();
9898
cookies.set('a', 'b', { secure: false, httpOnly: false, sameSite: 'strict', path: '/a/b/c' });
99-
const opts = new_cookies['a']?.options;
99+
const opts = new_cookies['/a/b/c?a']?.options;
100100
assert.equal(opts?.secure, false);
101101
assert.equal(opts?.httpOnly, false);
102102
assert.equal(opts?.path, '/a/b/c');
@@ -106,7 +106,7 @@ test('overridden defaults when set is called', () => {
106106
test('default values when delete is called', () => {
107107
const { cookies, new_cookies } = cookies_setup();
108108
cookies.delete('a', { path: '/' });
109-
const opts = new_cookies['a']?.options;
109+
const opts = new_cookies['/?a']?.options;
110110
assert.equal(opts?.secure, true);
111111
assert.equal(opts?.httpOnly, true);
112112
assert.equal(opts?.path, '/');
@@ -117,7 +117,7 @@ test('default values when delete is called', () => {
117117
test('overridden defaults when delete is called', () => {
118118
const { cookies, new_cookies } = cookies_setup();
119119
cookies.delete('a', { secure: false, httpOnly: false, sameSite: 'strict', path: '/a/b/c' });
120-
const opts = new_cookies['a']?.options;
120+
const opts = new_cookies['/a/b/c?a']?.options;
121121
assert.equal(opts?.secure, false);
122122
assert.equal(opts?.httpOnly, false);
123123
assert.equal(opts?.path, '/a/b/c');
@@ -128,15 +128,15 @@ test('overridden defaults when delete is called', () => {
128128
test('cannot override maxAge on delete', () => {
129129
const { cookies, new_cookies } = cookies_setup();
130130
cookies.delete('a', { path: '/', maxAge: 1234 });
131-
const opts = new_cookies['a']?.options;
131+
const opts = new_cookies['/?a']?.options;
132132
assert.equal(opts?.maxAge, 0);
133133
});
134134

135135
test('last cookie set with the same name wins', () => {
136136
const { cookies, new_cookies } = cookies_setup();
137137
cookies.set('a', 'foo', { path: '/' });
138138
cookies.set('a', 'bar', { path: '/' });
139-
const entry = new_cookies['a'];
139+
const entry = new_cookies['/?a'];
140140
assert.equal(entry?.value, 'bar');
141141
});
142142

@@ -145,8 +145,8 @@ test('cookie names are case sensitive', () => {
145145
// not that one should do this, but we follow the spec...
146146
cookies.set('a', 'foo', { path: '/' });
147147
cookies.set('A', 'bar', { path: '/' });
148-
const entrya = new_cookies['a'];
149-
const entryA = new_cookies['A'];
148+
const entrya = new_cookies['/?a'];
149+
const entryA = new_cookies['/?A'];
150150
assert.equal(entrya?.value, 'foo');
151151
assert.equal(entryA?.value, 'bar');
152152
});
@@ -211,5 +211,23 @@ test("set_internal isn't affected by defaults", () => {
211211
set_internal('test', 'foo', options);
212212

213213
expect(cookies.get('test')).toEqual('foo');
214-
expect(new_cookies['test']?.options).toEqual(options);
214+
expect(new_cookies['/a/b/c?test']?.options).toEqual(options);
215+
});
216+
217+
test('set same name in different path', () => {
218+
const { cookies, new_cookies } = cookies_setup();
219+
220+
cookies.set('a', '1', { path: '/foo' });
221+
cookies.set('a', '2', { path: '/bar' });
222+
expect(new_cookies['/bar?a'].name).toEqual('a');
223+
expect(new_cookies['/bar?a'].value).toEqual('2');
224+
expect(new_cookies['/foo?a'].name).toEqual('a');
225+
expect(new_cookies['/foo?a'].value).toEqual('1');
226+
});
227+
228+
test('set cookie to specific domain and path', () => {
229+
const { cookies } = cookies_setup();
230+
231+
cookies.set('a', 'b', { path: '/a/b/c', domain: 'test.com' });
232+
expect(cookies.get('a', undefined, { domain: 'test.com', path: '/a/b/c' })).toEqual('b');
215233
});

packages/kit/types/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,13 @@ declare module '@sveltejs/kit' {
189189
* Gets a cookie that was previously set with `cookies.set`, or from the request headers.
190190
* @param name the name of the cookie
191191
* @param opts the options, passed directly to `cookie.parse`. See documentation [here](https://github.com/jshttp/cookie#cookieparsestr-options)
192+
* @param target the target, used to determine the domain and path of the cookie.
192193
*/
193-
get: (name: string, opts?: import('cookie').CookieParseOptions) => string | undefined;
194+
get: (
195+
name: string,
196+
opts?: import('cookie').CookieParseOptions,
197+
target?: { domain?: string; path?: string }
198+
) => string | undefined;
194199

195200
/**
196201
* Gets all cookies that were previously set with `cookies.set`, or from the request headers.

0 commit comments

Comments
 (0)