Skip to content

Commit

Permalink
Allow setting all cookie package serialize/parse options (#9063)
Browse files Browse the repository at this point in the history
* #9062: allow setting all cookie package serialize/parse options

* 9062: fix scripts to original arrangement

* feat: only add specific properties

* Update tiny-days-dance.md

* Add examples to the changeset

* Update .changeset/tiny-days-dance.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/tiny-days-dance.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/tiny-days-dance.md

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Update .changeset/tiny-days-dance.md

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

---------

Co-authored-by: Arsh <69170106+lilnasy@users.noreply.github.com>
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
  • Loading branch information
7 people authored Jan 3, 2024
1 parent 08402ad commit f33fe31
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 10 deletions.
28 changes: 28 additions & 0 deletions .changeset/tiny-days-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'astro': minor
---

Cookie encoding / decoding can now be customized

Adds new `encode` and `decode` functions to allow customizing how cookies are encoded and decoded. For example, you can bypass the default encoding via `encodeURIComponent` when adding a URL as part of a cookie:

```astro
---
import { encodeCookieValue } from "./cookies";
Astro.cookies.set('url', Astro.url.toString(), {
// Override the default encoding so that URI components are not encoded
encode: value => encodeCookieValue(value)
});
---
```

Later, you can decode the URL in the same way:

```astro
---
import { decodeCookieValue } from "./cookies";
const url = Astro.cookies.get('url', {
decode: value => decodeCookieValue(value)
});
---
```
2 changes: 1 addition & 1 deletion packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type {
} from '../assets/types.js';
export type { RemotePattern } from '../assets/utils/remotePattern.js';
export type { SSRManifest } from '../core/app/types.js';
export type { AstroCookies } from '../core/cookies/index.js';
export type { AstroCookies, AstroCookieSetOptions, AstroCookieGetOptions } from '../core/cookies/index.js';

export interface AstroBuiltinProps {
'client:load'?: boolean;
Expand Down
23 changes: 14 additions & 9 deletions packages/astro/src/core/cookies/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { CookieSerializeOptions } from 'cookie';
import { parse, serialize } from 'cookie';
import { AstroError, AstroErrorData } from '../errors/index.js';

interface AstroCookieSetOptions {
export interface AstroCookieSetOptions {
domain?: string;
expires?: Date;
httpOnly?: boolean;
maxAge?: number;
path?: string;
sameSite?: boolean | 'lax' | 'none' | 'strict';
secure?: boolean;
encode?: (value: string) => string;
}

export interface AstroCookieGetOptions {
decode?: (value: string) => string;
};

type AstroCookieDeleteOptions = Pick<AstroCookieSetOptions, 'domain' | 'path'>;

interface AstroCookieInterface {
Expand Down Expand Up @@ -97,7 +102,7 @@ class AstroCookies implements AstroCookiesInterface {
* @param key The cookie to get.
* @returns An object containing the cookie value as well as convenience methods for converting its value.
*/
get(key: string): AstroCookie | undefined {
get(key: string, options: AstroCookieGetOptions | undefined = undefined): AstroCookie | undefined {
// Check for outgoing Set-Cookie values first
if (this.#outgoing?.has(key)) {
let [serializedValue, , isSetValue] = this.#outgoing.get(key)!;
Expand All @@ -108,7 +113,7 @@ class AstroCookies implements AstroCookiesInterface {
}
}

const values = this.#ensureParsed();
const values = this.#ensureParsed(options);
if (key in values) {
const value = values[key];
return new AstroCookie(value);
Expand All @@ -121,12 +126,12 @@ class AstroCookies implements AstroCookiesInterface {
* @param key The cookie to check for.
* @returns
*/
has(key: string): boolean {
has(key: string, options: AstroCookieGetOptions | undefined = undefined): boolean {
if (this.#outgoing?.has(key)) {
let [, , isSetValue] = this.#outgoing.get(key)!;
return isSetValue;
}
const values = this.#ensureParsed();
const values = this.#ensureParsed(options);
return !!values[key];
}

Expand Down Expand Up @@ -185,9 +190,9 @@ class AstroCookies implements AstroCookiesInterface {
}
}

#ensureParsed(): Record<string, string> {
#ensureParsed(options: AstroCookieGetOptions | undefined = undefined): Record<string, string> {
if (!this.#requestValues) {
this.#parse();
this.#parse(options);
}
if (!this.#requestValues) {
this.#requestValues = {};
Expand All @@ -202,13 +207,13 @@ class AstroCookies implements AstroCookiesInterface {
return this.#outgoing;
}

#parse() {
#parse(options: AstroCookieGetOptions | undefined = undefined) {
const raw = this.#request.headers.get('cookie');
if (!raw) {
return;
}

this.#requestValues = parse(raw);
this.#requestValues = parse(raw, options);
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/cookies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export {
getSetCookiesFromResponse,
responseHasCookies,
} from './response.js';
export type { AstroCookieSetOptions, AstroCookieGetOptions } from "./cookies.js";
24 changes: 24 additions & 0 deletions packages/astro/test/units/cookies/get.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ describe('astro/src/core/cookies', () => {
expect(cookies.get('foo').value).to.equal('bar');
});

it('gets the cookie value with default decode', () => {
const url = 'http://localhost';
const req = new Request('http://example.com/', {
headers: {
cookie: `url=${encodeURIComponent(url)}`,
},
});
const cookies = new AstroCookies(req);
// by default decodeURIComponent is used on the value
expect(cookies.get('url').value).to.equal(url);
});

it('gets the cookie value with custom decode', () => {
const url = 'http://localhost';
const req = new Request('http://example.com/', {
headers: {
cookie: `url=${encodeURIComponent(url)}`,
},
});
const cookies = new AstroCookies(req);
// set decode to the identity function to prevent decodeURIComponent on the value
expect(cookies.get('url', { decode: o => o }).value).to.equal(encodeURIComponent(url));
});

it("Returns undefined is the value doesn't exist", () => {
const req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
Expand Down
23 changes: 23 additions & 0 deletions packages/astro/test/units/cookies/set.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,29 @@ describe('astro/src/core/cookies', () => {
expect(headers[0]).to.equal('foo=bar');
});

it('Sets a cookie value that can be serialized w/ defaultencodeURIComponent behavior', () => {
let req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
const url = 'http://localhost/path';
cookies.set('url', url);
let headers = Array.from(cookies.headers());
expect(headers).to.have.a.lengthOf(1);
// by default cookie value is URI encoded
expect(headers[0]).to.equal(`url=${encodeURIComponent(url)}`);
});

it('Sets a cookie value that can be serialized w/ custom encode behavior', () => {
let req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
const url = 'http://localhost/path';
// set encode option to the identity function
cookies.set('url', url, { encode: o => o });
let headers = Array.from(cookies.headers());
expect(headers).to.have.a.lengthOf(1);
// no longer URI encoded
expect(headers[0]).to.equal(`url=${url}`);
});

it('Can set cookie options', () => {
let req = new Request('http://example.com/');
let cookies = new AstroCookies(req);
Expand Down

0 comments on commit f33fe31

Please sign in to comment.