Skip to content

Commit c14f3ae

Browse files
fix: cache mechanism for request with different headers (#8754)
* Fix cache mechanism for request with different headers. * add headers to build_selector hash; * update serialize_data to include req headers; * remove data-hash if req data is an empty obj; * fix failing tests and update harcoded hash; * add new test; * update hash function to support multiple params; * fmt * simplify * remove runtime assertion, use for-of loop instead of forEach * simplify * simplify test * revert hash changes * Create silent-planes-smell.md --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 32cbe07 commit c14f3ae

File tree

12 files changed

+107
-17
lines changed

12 files changed

+107
-17
lines changed

.changeset/silent-planes-smell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": patch
3+
---
4+
5+
fix: consider headers when constructing request hash

packages/kit/src/runtime/client/fetcher.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,19 @@ function build_selector(resource, opts) {
127127

128128
let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
129129

130-
if (opts?.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
131-
selector += `[data-hash="${hash(opts.body)}"]`;
130+
if (opts?.headers || opts?.body) {
131+
/** @type {import('types').StrictBody[]} */
132+
const values = [];
133+
134+
if (opts.headers) {
135+
values.push([...new Headers(opts.headers)].join(','));
136+
}
137+
138+
if (opts.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
139+
values.push(opts.body);
140+
}
141+
142+
selector += `[data-hash="${hash(...values)}"]`;
132143
}
133144

134145
return selector;

packages/kit/src/runtime/hash.js

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/**
22
* Hash using djb2
3-
* @param {import('types').StrictBody} value
3+
* @param {import('types').StrictBody[]} values
44
*/
5-
export function hash(value) {
5+
export function hash(...values) {
66
let hash = 5381;
77

8-
if (typeof value === 'string') {
9-
let i = value.length;
10-
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
11-
} else if (ArrayBuffer.isView(value)) {
12-
const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
13-
let i = buffer.length;
14-
while (i) hash = (hash * 33) ^ buffer[--i];
15-
} else {
16-
throw new TypeError('value must be a string or TypedArray');
8+
for (const value of values) {
9+
if (typeof value === 'string') {
10+
let i = value.length;
11+
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
12+
} else if (ArrayBuffer.isView(value)) {
13+
const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
14+
let i = buffer.length;
15+
while (i) hash = (hash * 33) ^ buffer[--i];
16+
} else {
17+
throw new TypeError('value must be a string or TypedArray');
18+
}
1719
}
1820

1921
return (hash >>> 0).toString(36);

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

+1
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
194194
? await stream_to_string(cloned_body)
195195
: init?.body
196196
),
197+
request_headers: init?.headers,
197198
response_body: body,
198199
response: response
199200
});

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,19 @@ export function serialize_data(fetched, filter, prerendering = false) {
7373
`data-url=${escape_html_attr(fetched.url)}`
7474
];
7575

76-
if (fetched.request_body) {
77-
attrs.push(`data-hash=${escape_html_attr(hash(fetched.request_body))}`);
76+
if (fetched.request_headers || fetched.request_body) {
77+
/** @type {import('types').StrictBody[]} */
78+
const values = [];
79+
80+
if (fetched.request_headers) {
81+
values.push([...new Headers(fetched.request_headers)].join(','));
82+
}
83+
84+
if (fetched.request_body) {
85+
values.push(fetched.request_body);
86+
}
87+
88+
attrs.push(`data-hash="${hash(...values)}"`);
7889
}
7990

8091
// Compute the time the response should be cached, taking into account max-age and age.

packages/kit/src/runtime/server/page/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Fetched {
55
url: string;
66
method: string;
77
request_body?: string | ArrayBufferView | null;
8+
request_headers?: HeadersInit | undefined;
89
response_body: string;
910
response: Response;
1011
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
<a href="/load/fetch-cache-control/load-data">load-data</a>
2+
3+
<a href="/load/fetch-cache-control/headers-diff">headers-diff</a>
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export async function load({ fetch, url }) {
2+
const r1 = await fetch(url.pathname, {
3+
headers: {
4+
'x-foo': 'a'
5+
}
6+
});
7+
8+
const r2 = await fetch(url.pathname, {
9+
headers: {
10+
'x-foo': 'b'
11+
}
12+
});
13+
14+
return {
15+
a: r1.json(),
16+
b: r2.json()
17+
};
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
export let data;
3+
</script>
4+
5+
<a href="/load/fetch-cache-control">fetch-cache-control</a>
6+
7+
<h2>{data.a.foo} / {data.b.foo}</h2>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { json } from '@sveltejs/kit';
2+
3+
/** @type {import('./$types').RequestHandler} */
4+
export async function GET({ request, setHeaders }) {
5+
setHeaders({
6+
'cache-control': 'public, max-age=7'
7+
});
8+
9+
return json({
10+
foo: request.headers.get('x-foo')
11+
});
12+
}

packages/kit/test/apps/basics/test/client.test.js

+19
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,25 @@ test.describe('Load', () => {
193193
expect(did_request_data).toBe(false);
194194
});
195195

196+
test('do not use cache if headers are different', async ({ page, clicknav }) => {
197+
await page.goto('/load/fetch-cache-control/headers-diff');
198+
199+
// 1. We expect the right data
200+
expect(await page.textContent('h2')).toBe('a / b');
201+
202+
// 2. Change to another route (client side)
203+
await clicknav('[href="/load/fetch-cache-control"]');
204+
205+
// 3. Come back to the original page (client side)
206+
const requests = [];
207+
page.on('request', (request) => requests.push(request));
208+
await clicknav('[href="/load/fetch-cache-control/headers-diff"]');
209+
210+
// 4. We expect the same data and no new request because it was cached.
211+
expect(await page.textContent('h2')).toBe('a / b');
212+
expect(requests).toEqual([]);
213+
});
214+
196215
if (process.env.DEV) {
197216
test('using window.fetch causes a warning', async ({ page, baseURL }) => {
198217
await Promise.all([

packages/kit/test/apps/basics/test/test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,10 @@ test.describe('Load', () => {
238238
const payload_b = '{"status":200,"statusText":"","headers":{},"body":"Y"}';
239239
// by the time JS has run, hydration will have nuked these scripts
240240
const script_contents_a = await page.innerHTML(
241-
'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="3t25"]'
241+
'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="1vn6nlx"]'
242242
);
243243
const script_contents_b = await page.innerHTML(
244-
'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="3t24"]'
244+
'script[data-sveltekit-fetched][data-url="/load/serialization-post.json"][data-hash="1vn6nlw"]'
245245
);
246246

247247
expect(script_contents_a).toBe(payload_a);

0 commit comments

Comments
 (0)