-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: support multiple cookies with the same name across different paths and domains #14131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
338cd54
ad4dc73
e5603da
c62add6
12df174
062b2d2
e4eb4de
08ebe85
a1b08c3
9d9f2ae
6b59699
0bb05c3
c056d6e
285451a
5764a8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sveltejs/kit': patch | ||
--- | ||
|
||
fix: support multiple cookies with the same name across different paths and domains |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sveltejs/kit': patch | ||
--- | ||
|
||
fix: `cookies.get(...)` returns `undefined` for a just-deleted cookie |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,21 @@ function validate_options(options) { | |
} | ||
} | ||
|
||
/** | ||
* Generates a unique key for a cookie based on its domain, path, and name in | ||
* the format: `<domain>/<path>?<name>`. | ||
* If domain is undefined, it will be omitted. | ||
* For example: `/?name`, `example.com/foo?name`. | ||
* | ||
* @param {string | undefined} domain | ||
* @param {string} path | ||
* @param {string} name | ||
* @returns {string} | ||
*/ | ||
function generate_cookie_key(domain, path, name) { | ||
return `${domain || ''}${path}?${name}`; | ||
} | ||
|
||
/** | ||
* @param {Request} request | ||
* @param {URL} url | ||
|
@@ -37,8 +52,8 @@ export function get_cookies(request, url) { | |
/** @type {string | undefined} */ | ||
let normalized_url; | ||
|
||
/** @type {Record<string, import('./page/types.js').Cookie>} */ | ||
const new_cookies = {}; | ||
/** @type {Map<string, import('./page/types.js').Cookie>} */ | ||
const new_cookies = new Map(); | ||
|
||
/** @type {import('cookie').CookieSerializeOptions} */ | ||
const defaults = { | ||
|
@@ -59,13 +74,19 @@ export function get_cookies(request, url) { | |
* @param {import('cookie').CookieParseOptions} [opts] | ||
*/ | ||
get(name, opts) { | ||
const c = new_cookies[name]; | ||
if ( | ||
c && | ||
domain_matches(url.hostname, c.options.domain) && | ||
path_matches(url.pathname, c.options.path) | ||
) { | ||
return c.value; | ||
// Look for the most specific matching cookie from new_cookies | ||
const best_match = Array.from(new_cookies.values()) | ||
.filter((c) => { | ||
return ( | ||
c.name === name && | ||
domain_matches(url.hostname, c.options.domain) && | ||
path_matches(url.pathname, c.options.path) | ||
); | ||
}) | ||
.sort((a, b) => b.options.path.length - a.options.path.length)[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing that sorting by length works because browsers don't send cookies for unrelated paths, but it won't work after the server creates a cookie for an unrelated path. Is this the remaining bug you mentioned @Rich-Harris ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah no. An unrelated path would've already been filtered out in the previous step. |
||
|
||
if (best_match) { | ||
return best_match.value || undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this return undefined if the cookie value happens to be an empty string? I'm not sure if that case needs to be considered. |
||
} | ||
|
||
const req_cookies = parse(header, { decode: opts?.decode }); | ||
|
@@ -97,15 +118,28 @@ export function get_cookies(request, url) { | |
getAll(opts) { | ||
const cookies = parse(header, { decode: opts?.decode }); | ||
|
||
for (const c of Object.values(new_cookies)) { | ||
// Group cookies by name and find the most specific one for each name | ||
const lookup = new Map(); | ||
|
||
for (const c of new_cookies.values()) { | ||
if ( | ||
domain_matches(url.hostname, c.options.domain) && | ||
path_matches(url.pathname, c.options.path) | ||
) { | ||
cookies[c.name] = c.value; | ||
const existing = lookup.get(c.name); | ||
|
||
// If no existing cookie or this one has a more specific (longer) path, use this one | ||
if (!existing || c.options.path.length > existing.options.path.length) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another path-lenth check that assumes that there's no possible longer but unrelated path in the store |
||
lookup.set(c.name, c); | ||
} | ||
} | ||
} | ||
|
||
// Add the most specific cookies to the result | ||
for (const c of lookup.values()) { | ||
cookies[c.name] = c.value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should a separate getAllRaw function be added to allow a server to truly get all cookies including duplicate named ones? |
||
} | ||
|
||
return Object.entries(cookies).map(([name, value]) => ({ name, value })); | ||
}, | ||
|
||
|
@@ -171,8 +205,7 @@ export function get_cookies(request, url) { | |
}; | ||
|
||
// cookies previous set during this event with cookies.set have higher precedence | ||
for (const key in new_cookies) { | ||
const cookie = new_cookies[key]; | ||
for (const cookie of new_cookies.values()) { | ||
if (!domain_matches(destination.hostname, cookie.options.domain)) continue; | ||
if (!path_matches(destination.pathname, cookie.options.path)) continue; | ||
|
||
|
@@ -213,10 +246,13 @@ export function get_cookies(request, url) { | |
path = resolve(normalized_url, path); | ||
} | ||
|
||
new_cookies[name] = { name, value, options: { ...options, path } }; | ||
// Generate unique key for cookie storage | ||
const cookie_key = generate_cookie_key(options.domain, path, name); | ||
const cookie = { name, value, options: { ...options, path } }; | ||
new_cookies.set(cookie_key, cookie); | ||
|
||
if (__SVELTEKIT_DEV__) { | ||
const serialized = serialize(name, value, new_cookies[name].options); | ||
const serialized = serialize(name, value, cookie.options); | ||
if (new TextEncoder().encode(serialized).byteLength > MAX_COOKIE_SIZE) { | ||
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`); | ||
} | ||
|
@@ -270,7 +306,7 @@ export function path_matches(path, constraint) { | |
|
||
/** | ||
* @param {Headers} headers | ||
* @param {import('./page/types.js').Cookie[]} cookies | ||
* @param {MapIterator<import('./page/types.js').Cookie>} cookies | ||
*/ | ||
export function add_cookies_to_headers(headers, cookies) { | ||
for (const new_cookie of cookies) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inputs might have to be url encoded to prevent ambiguity. For example:
/foo?
?
name
, and/foo
?
?name
are currently not distinguishable.