Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cookies-get-cached-parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

perf: cache the default cookie header parse and avoid allocations in `cookies.get`
50 changes: 34 additions & 16 deletions packages/kit/src/runtime/server/cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ export function get_cookies(request, url) {
parseCookie(header, { decode: (value) => value })
);

/** @type {ReturnType<typeof parseCookie> | undefined} */
let default_cookies;

/**
* The header never changes during the request, so the default-decode parse is cached
* @param {import('cookie').ParseOptions} [opts]
*/
function parse_header(opts) {
return opts?.decode ? parseCookie(header, opts) : (default_cookies ??= parseCookie(header));
}

/** @param {import('./page/types.js').Cookie} cookie */
function matches_url(cookie) {
return (
domain_matches(url.hostname, cookie.options.domain) &&
path_matches(url.pathname, cookie.options.path)
);
}

/** @type {string | undefined} */
let normalized_url;

Expand All @@ -66,22 +85,23 @@ export function get_cookies(request, url) {

get(name, opts) {
// 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];
/** @type {import('./page/types.js').Cookie | undefined} */
let best_match;
for (const c of new_cookies.values()) {
if (
c.name === name &&
matches_url(c) &&
(!best_match || c.options.path.length > best_match.options.path.length)
) {
best_match = c;
}
}

if (best_match) {
return best_match.options.maxAge === 0 ? undefined : best_match.value;
}

const req_cookies = parseCookie(header, { decode: opts?.decode });
const cookie = req_cookies[name]; // the decoded string or undefined
const cookie = parse_header(opts)[name]; // the decoded string or undefined

// in development, if the cookie was set during this session with `cookies.set`,
// but at a different path, warn the user. (ignore cookies from request headers,
Expand All @@ -104,16 +124,14 @@ export function get_cookies(request, url) {
},

getAll(opts) {
const cookies = parseCookie(header, { decode: opts?.decode });
// copy, so the cached parse isn't mutated below
const cookies = { ...parse_header(opts) };

// 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)
) {
if (matches_url(c)) {
const existing = lookup.get(c.name);

// If no existing cookie or this one has a more specific (longer) path, use this one
Expand Down
8 changes: 8 additions & 0 deletions packages/kit/src/runtime/server/cookie.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ describe.skipIf(process.env.NODE_ENV !== 'production')('cookies in prod', () =>
]);
});

test('get with a custom decode is not served from the cached default parse', () => {
const { cookies } = cookies_setup({ headers: { cookie: 'enc=hello%20world' } });

expect(cookies.get('enc')).toEqual('hello world');
expect(cookies.get('enc', { decode: (value) => value })).toEqual('hello%20world');
expect(cookies.get('enc')).toEqual('hello world');
});

test("set_internal isn't affected by defaults", () => {
const { cookies, new_cookies, set_internal } = cookies_setup({
href: 'https://example.com/a/b/c'
Expand Down
Loading