diff --git a/.changeset/silent-planes-smell.md b/.changeset/silent-planes-smell.md new file mode 100644 index 000000000000..df9173436bb6 --- /dev/null +++ b/.changeset/silent-planes-smell.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": patch +--- + +fix: consider headers when constructing request hash diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 90db3fe4456c..e73da8f2ca51 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -127,8 +127,19 @@ function build_selector(resource, opts) { let selector = `script[data-sveltekit-fetched][data-url=${url}]`; - if (opts?.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) { - selector += `[data-hash="${hash(opts.body)}"]`; + if (opts?.headers || opts?.body) { + /** @type {import('types').StrictBody[]} */ + const values = []; + + if (opts.headers) { + values.push([...new Headers(opts.headers)].join(',')); + } + + if (opts.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) { + values.push(opts.body); + } + + selector += `[data-hash="${hash(...values)}"]`; } return selector; diff --git a/packages/kit/src/runtime/hash.js b/packages/kit/src/runtime/hash.js index 0056251d3462..cfebacb382fa 100644 --- a/packages/kit/src/runtime/hash.js +++ b/packages/kit/src/runtime/hash.js @@ -1,19 +1,21 @@ /** * Hash using djb2 - * @param {import('types').StrictBody} value + * @param {import('types').StrictBody[]} values */ -export function hash(value) { +export function hash(...values) { let hash = 5381; - if (typeof value === 'string') { - let i = value.length; - while (i) hash = (hash * 33) ^ value.charCodeAt(--i); - } else if (ArrayBuffer.isView(value)) { - const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); - let i = buffer.length; - while (i) hash = (hash * 33) ^ buffer[--i]; - } else { - throw new TypeError('value must be a string or TypedArray'); + for (const value of values) { + if (typeof value === 'string') { + let i = value.length; + while (i) hash = (hash * 33) ^ value.charCodeAt(--i); + } else if (ArrayBuffer.isView(value)) { + const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + let i = buffer.length; + while (i) hash = (hash * 33) ^ buffer[--i]; + } else { + throw new TypeError('value must be a string or TypedArray'); + } } return (hash >>> 0).toString(36); diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 31af151aee3c..0c212fa9e604 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -194,6 +194,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts) ? await stream_to_string(cloned_body) : init?.body ), + request_headers: init?.headers, response_body: body, response: response }); diff --git a/packages/kit/src/runtime/server/page/serialize_data.js b/packages/kit/src/runtime/server/page/serialize_data.js index 5b275b0c6bea..f176fdfb41a0 100644 --- a/packages/kit/src/runtime/server/page/serialize_data.js +++ b/packages/kit/src/runtime/server/page/serialize_data.js @@ -73,8 +73,19 @@ export function serialize_data(fetched, filter, prerendering = false) { `data-url=${escape_html_attr(fetched.url)}` ]; - if (fetched.request_body) { - attrs.push(`data-hash=${escape_html_attr(hash(fetched.request_body))}`); + if (fetched.request_headers || fetched.request_body) { + /** @type {import('types').StrictBody[]} */ + const values = []; + + if (fetched.request_headers) { + values.push([...new Headers(fetched.request_headers)].join(',')); + } + + if (fetched.request_body) { + values.push(fetched.request_body); + } + + attrs.push(`data-hash="${hash(...values)}"`); } // Compute the time the response should be cached, taking into account max-age and age. diff --git a/packages/kit/src/runtime/server/page/types.d.ts b/packages/kit/src/runtime/server/page/types.d.ts index ccd13373a6c3..2bb6c89808a7 100644 --- a/packages/kit/src/runtime/server/page/types.d.ts +++ b/packages/kit/src/runtime/server/page/types.d.ts @@ -5,6 +5,7 @@ export interface Fetched { url: string; method: string; request_body?: string | ArrayBufferView | null; + request_headers?: HeadersInit | undefined; response_body: string; response: Response; } diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte index 1148676b0b33..059f911b8567 100644 --- a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/+page.svelte @@ -1 +1,4 @@ load-data + +headers-diff + diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.js b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.js new file mode 100644 index 000000000000..b64c9e0a9a68 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.js @@ -0,0 +1,18 @@ +export async function load({ fetch, url }) { + const r1 = await fetch(url.pathname, { + headers: { + 'x-foo': 'a' + } + }); + + const r2 = await fetch(url.pathname, { + headers: { + 'x-foo': 'b' + } + }); + + return { + a: r1.json(), + b: r2.json() + }; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.svelte new file mode 100644 index 000000000000..8aa12f6b0843 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/fetch-cache-control/headers-diff/+page.svelte @@ -0,0 +1,7 @@ + + +fetch-cache-control + +