diff --git a/.changeset/sharp-birds-invent.md b/.changeset/sharp-birds-invent.md new file mode 100644 index 000000000000..e1bdbc421f15 --- /dev/null +++ b/.changeset/sharp-birds-invent.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: use `window.fetch` in `load` functions to allow libraries to patch it diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index df709d1f3968..b31a88a2a99f 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -23,6 +23,10 @@ if (DEV) { check_stack_trace(); + /** + * @param {RequestInfo | URL} input + * @param {RequestInit & Record | undefined} init + */ window.fetch = (input, init) => { // Check if fetch was called via load_node. the lock method only checks if it was called at the // same time, but not necessarily if it was called from `load`. @@ -36,10 +40,14 @@ if (DEV) { const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load')); const stack = stack_array.slice(0, cutoff + 2).join('\n'); - const heuristic = can_inspect_stack_trace + const in_load_heuristic = can_inspect_stack_trace ? stack.includes('src/runtime/client/client.js') : loading; - if (heuristic) { + + // This flag is set in initial_fetch and subsequent_fetch + const used_kit_fetch = init?.__sveltekit_fetch__; + + if (in_load_heuristic && !used_kit_fetch) { console.warn( `Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://kit.svelte.dev/docs/load#making-fetch-requests` ); @@ -86,7 +94,7 @@ export function initial_fetch(resource, opts) { return Promise.resolve(new Response(body, init)); } - return native_fetch(resource, opts); + return DEV ? dev_fetch(resource, opts) : window.fetch(resource, opts); } /** @@ -112,7 +120,22 @@ export function subsequent_fetch(resource, resolved, opts) { } } - return native_fetch(resolved, opts); + return DEV ? dev_fetch(resolved, opts) : window.fetch(resolved, opts); +} + +/** + * @param {RequestInfo | URL} resource + * @param {RequestInit & Record | undefined} opts + */ +function dev_fetch(resource, opts) { + const patched_opts = { ...opts }; + // This assigns the __sveltekit_fetch__ flag and makes it non-enumerable + Object.defineProperty(patched_opts, '__sveltekit_fetch__', { + value: true, + writable: true, + configurable: true + }); + return window.fetch(resource, patched_opts); } /** diff --git a/packages/kit/test/apps/basics/src/routes/load/window-fetch/patching/+page.js b/packages/kit/test/apps/basics/src/routes/load/window-fetch/patching/+page.js new file mode 100644 index 000000000000..115d5e6891cc --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/window-fetch/patching/+page.js @@ -0,0 +1,20 @@ +import { browser } from '$app/environment'; + +/** @type {import('./$types').PageLoad} */ +export async function load({ url, fetch }) { + // simulate fetch being monkey-patched by a 3rd party library + // run everything only in browser to avoid SSR caching + if (browser) { + const original_fetch = window.fetch; + window.fetch = (input, init) => { + console.log('Called a patched window.fetch'); + return original_fetch(input, init); + }; + + const res = await fetch(`${url.origin}/load/window-fetch/data.json`); + const { answer } = await res.json(); + window.fetch = original_fetch; + return { answer }; + } + return {}; +} diff --git a/packages/kit/test/apps/basics/src/routes/load/window-fetch/patching/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/window-fetch/patching/+page.svelte new file mode 100644 index 000000000000..7c7268b87da6 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/load/window-fetch/patching/+page.svelte @@ -0,0 +1,6 @@ + + +

{data.answer}

diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 74e90d760616..aac4812f51c1 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -228,6 +228,21 @@ test.describe('Load', () => { expect(requests).toEqual([]); }); + test('permits 3rd party patching of fetch in universal load functions', async ({ page }) => { + /** @type {string[]} */ + const logs = []; + page.on('console', (msg) => { + if (msg.type() === 'log') { + logs.push(msg.text()); + } + }); + + await page.goto('/load/window-fetch/patching'); + expect(await page.textContent('h1')).toBe('42'); + + expect(logs).toContain('Called a patched window.fetch'); + }); + if (process.env.DEV) { test('using window.fetch causes a warning', async ({ page, baseURL }) => { await Promise.all([