Skip to content

feat: add cookies.setFromString to create a cookie from a string #13681

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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/red-bikes-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `cookie.setFromString()` to set a cookie from a string
11 changes: 11 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,17 @@ export interface Cookies {
opts: import('cookie').CookieSerializeOptions & { path: string }
) => void;

/**
* Sets a cookie from a string. This will add a `set-cookie` header to the response, but also make the cookie available via `cookies.get` or `cookies.getAll` during the current request.
*
* No default values. It will set only properties you specified in a cookie.
*
* If you do not specify name, value and path, it will throw an error.
* @param cookie the cookie represented as string
* @since 2.21.0
*/
setFromString: (cookie: string) => void;

/**
* Deletes a cookie by setting its value to an empty string and setting the expiry date in the past.
*
Expand Down
39 changes: 39 additions & 0 deletions packages/kit/src/runtime/server/cookie.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parse, serialize } from 'cookie';
import { normalize_path, resolve } from '../../utils/url.js';
import { add_data_suffix } from '../pathname.js';
import * as set_cookie_parser from 'set-cookie-parser';

// eslint-disable-next-line no-control-regex -- control characters are invalid in cookie names
const INVALID_COOKIE_CHARACTER_REGEX = /[\x00-\x1F\x7F()<>@,;:"/[\]?={} \t]/;
Expand Down Expand Up @@ -129,6 +130,44 @@ export function get_cookies(request, url) {
set_internal(name, value, { ...defaults, ...options });
},

/**
* @param {string} cookie
*/
setFromString(cookie) {
if (cookie === '') {
throw new Error('Cannot pass empty string');
}

const parsed = set_cookie_parser.parseString(cookie);
const { name, value, path, sameSite, secure, httpOnly, ...opts } = parsed;

if (name === undefined || value === undefined || path === undefined) {
throw new Error('Name, Value and Path are mandatory for cookie to be created.');
}

/**
* @type {true|false|"lax"|"strict"|"none"|undefined}
*/
const normalized_same_site = (() => {
if (sameSite === undefined || typeof sameSite === 'boolean') {
return sameSite;
}
const lower = sameSite.toLowerCase();
if (lower === 'lax' || lower === 'strict' || lower === 'none') {
return /** @type {"lax"|"strict"|"none"} */ (lower);
}
return undefined;
})();

this.set(name, value, {
...opts,
path,
sameSite: normalized_same_site,
secure: secure ?? false,
httpOnly: httpOnly ?? false
});
},

/**
* @param {string} name
* @param {import('./page/types.js').Cookie['options']} options
Expand Down
40 changes: 40 additions & 0 deletions packages/kit/src/runtime/server/cookie.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,43 @@ test("set_internal isn't affected by defaults", () => {
expect(cookies.get('test')).toEqual('foo');
expect(new_cookies['test']?.options).toEqual(options);
});

test('no default values when setFromString is called', () => {
const { cookies, new_cookies } = cookies_setup();
const cookie_string = 'a=b; Path=/;';
cookies.setFromString(cookie_string);
const opts = new_cookies['a']?.options;
assert.equal(opts?.path, '/');
assert.equal(opts?.secure, false);
assert.equal(opts?.httpOnly, false);
assert.equal(opts?.sameSite, undefined);
});

test('set all parameters when setFromString is called', () => {
const { cookies, new_cookies } = cookies_setup();
const cookie_string =
'a=b; Path=/; Max-Age=3600; Expires=Thu, 03 Apr 2025 00:41:07 GMT; Secure; HttpOnly; SameSite=Strict; domain=example.com';
cookies.setFromString(cookie_string);
const opts = new_cookies['a']?.options;
assert.equal(opts?.path, '/');
assert.equal(opts?.secure, true);
assert.equal(opts?.httpOnly, true);
assert.equal(opts?.sameSite, 'strict');
assert.equal(opts?.domain, 'example.com');
assert.equal(opts?.maxAge, 3600);
assert.isNotNull(opts.expires);
});

test('throw error when setFromString is called with empty string', () => {
const { cookies } = cookies_setup();
assert.throws(() => cookies.setFromString(''), 'Cannot pass empty string');
});

test('throw error when setFromString is called without name, value and path', () => {
const { cookies } = cookies_setup();
const cookie_string = 'Max-Age=3600; Expires=Thu, 03 Apr 2025 00:41:07 GMT; Secure; HttpOnly;';
assert.throws(
() => cookies.setFromString(cookie_string),
'Name, Value and Path are mandatory for cookie to be created.'
);
});
11 changes: 11 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ declare module '@sveltejs/kit' {
opts: import('cookie').CookieSerializeOptions & { path: string }
) => void;

/**
* Sets a cookie from a string. This will add a `set-cookie` header to the response, but also make the cookie available via `cookies.get` or `cookies.getAll` during the current request.
*
* No default values. It will set only properties you specified in a cookie.
*
* If you do not specify name, value and path, it will throw an error.
* @param cookie the cookie represented as string
* @since 2.21.0
*/
setFromString: (cookie: string) => void;

/**
* Deletes a cookie by setting its value to an empty string and setting the expiry date in the past.
*
Expand Down