Skip to content

Commit 3f0ae14

Browse files
apply set-cookie headers from page dependencies (#4588)
* apply set-cookie headers from page dependencies - fixes #1198 * hmmm * deps * accommodate commonjs laggards * Update packages/adapter-static/package.json Co-authored-by: Ben McCann <[email protected]> * fix lockfile Co-authored-by: Ben McCann <[email protected]>
1 parent f59abf5 commit 3f0ae14

File tree

12 files changed

+197
-11
lines changed

12 files changed

+197
-11
lines changed

.changeset/eight-files-think.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+
Apply set-cookie headers from page dependencies

packages/adapter-static/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
},
3030
"devDependencies": {
3131
"@sveltejs/kit": "workspace:*",
32+
"cookie": "^0.5.0",
3233
"devalue": "^2.0.1",
3334
"playwright-chromium": "^1.21.0",
3435
"port-authority": "^1.1.2",
36+
"set-cookie-parser": "^2.4.8",
3537
"sirv": "^2.0.0",
3638
"svelte": "^3.44.2",
3739
"typescript": "^4.6.2",

packages/kit/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@types/mime": "^2.0.3",
2424
"@types/node": "^16.11.11",
2525
"@types/sade": "^1.7.3",
26+
"@types/set-cookie-parser": "^2.4.2",
2627
"amphtml-validator": "^1.0.35",
2728
"cookie": "^0.5.0",
2829
"cross-env": "^7.0.3",
@@ -36,6 +37,7 @@
3637
"port-authority": "^1.1.2",
3738
"rollup": "^2.60.2",
3839
"selfsigned": "^2.0.0",
40+
"set-cookie-parser": "^2.4.8",
3941
"sirv": "^2.0.0",
4042
"svelte": "^3.44.2",
4143
"svelte-check": "^2.5.0",

packages/kit/rollup.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export default [
5353
plugins: [
5454
resolve({
5555
extensions: ['.mjs', '.js', '.ts']
56-
})
56+
}),
57+
commonjs()
5758
]
5859
},
5960

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @param {string} hostname
3+
* @param {string} [constraint]
4+
*/
5+
export function domain_matches(hostname, constraint) {
6+
if (!constraint) return true;
7+
8+
const normalized = constraint[0] === '.' ? constraint.slice(1) : constraint;
9+
10+
if (hostname === normalized) return true;
11+
return hostname.endsWith('.' + normalized);
12+
}
13+
14+
/**
15+
* @param {string} path
16+
* @param {string} [constraint]
17+
*/
18+
export function path_matches(path, constraint) {
19+
if (!constraint) return true;
20+
21+
const normalized = constraint.endsWith('/') ? constraint.slice(0, -1) : constraint;
22+
23+
if (path === normalized) return true;
24+
return path.startsWith(normalized + '/');
25+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { test } from 'uvu';
2+
import * as assert from 'uvu/assert';
3+
import { domain_matches, path_matches } from './cookie.js';
4+
5+
const domains = {
6+
positive: [
7+
['localhost'],
8+
['example.com', 'example.com'],
9+
['sub.example.com', 'example.com'],
10+
['example.com', '.example.com'],
11+
['sub.example.com', '.example.com']
12+
]
13+
};
14+
15+
const paths = {
16+
positive: [['/'], ['/foo', '/'], ['/foo', '/foo'], ['/foo/', '/foo'], ['/foo', '/foo/']],
17+
18+
negative: [
19+
['/', '/foo'],
20+
['/food', '/foo']
21+
]
22+
};
23+
24+
domains.positive.forEach(([hostname, constraint]) => {
25+
test(`${hostname} / ${constraint}`, () => {
26+
assert.ok(domain_matches(hostname, constraint));
27+
});
28+
});
29+
30+
paths.positive.forEach(([path, constraint]) => {
31+
test(`${path} / ${constraint}`, () => {
32+
assert.ok(path_matches(path, constraint));
33+
});
34+
});
35+
36+
paths.negative.forEach(([path, constraint]) => {
37+
test(`! ${path} / ${constraint}`, () => {
38+
assert.ok(!path_matches(path, constraint));
39+
});
40+
});
41+
42+
test.run();

packages/kit/src/runtime/server/page/load_node.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import * as cookie from 'cookie';
2+
import * as set_cookie_parser from 'set-cookie-parser';
13
import { normalize } from '../../load.js';
24
import { respond } from '../index.js';
35
import { is_root_relative, resolve } from '../../../utils/url.js';
46
import { create_prerendering_url_proxy } from './utils.js';
57
import { is_pojo, lowercase_keys, normalize_request_method } from '../utils.js';
68
import { coalesce_to_error } from '../../../utils/error.js';
9+
import { domain_matches, path_matches } from './cookie.js';
710

811
/**
912
* @param {{
@@ -41,10 +44,10 @@ export async function load_node({
4144
/** @type {Array<import('./types').Fetched>} */
4245
const fetched = [];
4346

44-
/**
45-
* @type {string[]}
46-
*/
47-
let set_cookie_headers = [];
47+
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
48+
49+
/** @type {import('set-cookie-parser').Cookie[]} */
50+
const new_cookies = [];
4851

4952
/** @type {import('types').LoadOutput} */
5053
let loaded;
@@ -60,7 +63,9 @@ export async function load_node({
6063
: {};
6164

6265
if (shadow.cookies) {
63-
set_cookie_headers.push(...shadow.cookies);
66+
shadow.cookies.forEach((header) => {
67+
new_cookies.push(set_cookie_parser.parseString(header));
68+
});
6469
}
6570

6671
if (shadow.error) {
@@ -166,9 +171,23 @@ export async function load_node({
166171
if (opts.credentials !== 'omit') {
167172
uses_credentials = true;
168173

169-
const cookie = event.request.headers.get('cookie');
170174
const authorization = event.request.headers.get('authorization');
171175

176+
// combine cookies from the initiating request with any that were
177+
// added via set-cookie
178+
const combined_cookies = { ...cookies };
179+
180+
for (const cookie of new_cookies) {
181+
if (!domain_matches(event.url.hostname, cookie.domain)) continue;
182+
if (!path_matches(resolved, cookie.path)) continue;
183+
184+
combined_cookies[cookie.name] = cookie.value;
185+
}
186+
187+
const cookie = Object.entries(combined_cookies)
188+
.map(([name, value]) => `${name}=${value}`)
189+
.join('; ');
190+
172191
if (cookie) {
173192
opts.headers.set('cookie', cookie);
174193
}
@@ -231,6 +250,15 @@ export async function load_node({
231250
response = await options.hooks.externalFetch.call(null, external_request);
232251
}
233252

253+
const set_cookie = response.headers.get('set-cookie');
254+
if (set_cookie) {
255+
new_cookies.push(
256+
...set_cookie_parser
257+
.splitCookiesString(set_cookie)
258+
.map((str) => set_cookie_parser.parseString(str))
259+
);
260+
}
261+
234262
const proxy = new Proxy(response, {
235263
get(response, key, _receiver) {
236264
async function text() {
@@ -239,9 +267,8 @@ export async function load_node({
239267
/** @type {import('types').ResponseHeaders} */
240268
const headers = {};
241269
for (const [key, value] of response.headers) {
242-
if (key === 'set-cookie') {
243-
set_cookie_headers = set_cookie_headers.concat(value);
244-
} else if (key !== 'etag') {
270+
// TODO skip others besides set-cookie and etag?
271+
if (key !== 'set-cookie' && key !== 'etag') {
245272
headers[key] = value;
246273
}
247274
}
@@ -362,7 +389,11 @@ export async function load_node({
362389
loaded: normalize(loaded),
363390
stuff: loaded.stuff || stuff,
364391
fetched,
365-
set_cookie_headers,
392+
set_cookie_headers: new_cookies.map((new_cookie) => {
393+
const { name, value, ...options } = new_cookie;
394+
// @ts-expect-error
395+
return cookie.serialize(name, value, options);
396+
}),
366397
uses_credentials
367398
};
368399
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @type {import('./a.json').RequestHandler} */
2+
export function get({ url }) {
3+
const answer = url.searchParams.get('answer') || '42';
4+
5+
return {
6+
headers: {
7+
'set-cookie': `answer=${answer}; HttpOnly; Path=/load/set-cookie-fetch`
8+
},
9+
body: {}
10+
};
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/** @type {import('./b.json').RequestHandler} */
2+
export function get({ request }) {
3+
const cookie = request.headers.get('cookie');
4+
5+
const match = /answer=([^;]+)/.exec(cookie);
6+
const answer = +match?.[1];
7+
8+
return {
9+
body: {
10+
answer
11+
},
12+
headers: {
13+
'set-cookie': `doubled=${answer * 2}; HttpOnly; Path=/load/set-cookie-fetch`
14+
}
15+
};
16+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script context="module">
2+
export async function load({ fetch, url }) {
3+
await fetch(`/load/set-cookie-fetch/a.json${url.search}`);
4+
const res = await fetch('/load/set-cookie-fetch/b.json');
5+
const { answer } = await res.json(); // need to read the response so it gets serialized
6+
7+
return {
8+
props: { answer }
9+
};
10+
}
11+
</script>
12+
13+
<script>
14+
/** @type {number} */
15+
export let answer;
16+
</script>
17+
18+
<h1>the answer is {answer}</h1>

0 commit comments

Comments
 (0)