diff --git a/.changeset/good-pigs-tap.md b/.changeset/good-pigs-tap.md new file mode 100644 index 000000000000..86d4401c2680 --- /dev/null +++ b/.changeset/good-pigs-tap.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +[fix] respect fetch cache option diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index e9654a7de87f..a5f170dfe3da 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -592,6 +592,7 @@ export function create_client({ target, base }) { uses.url = true; }), async fetch(resource, init) { + /** @type {URL | string} */ let requested; if (resource instanceof Request) { diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 31cf1567804a..fb89843f8505 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -41,9 +41,7 @@ if (import.meta.env.DEV) { const method = input instanceof Request ? input.method : init?.method || 'GET'; if (method !== 'GET') { - const url = new URL(input instanceof Request ? input.url : input.toString(), document.baseURI) - .href; - cache.delete(url); + cache.delete(build_selector(input)); } return native_fetch(input, init); @@ -53,9 +51,7 @@ if (import.meta.env.DEV) { const method = input instanceof Request ? input.method : init?.method || 'GET'; if (method !== 'GET') { - const url = new URL(input instanceof Request ? input.url : input.toString(), document.baseURI) - .href; - cache.delete(url); + cache.delete(build_selector(input)); } return native_fetch(input, init); @@ -67,7 +63,7 @@ const cache = new Map(); /** * Should be called on the initial run of load functions that hydrate the page. * Saves any requests with cache-control max-age to the cache. - * @param {RequestInfo | URL} resource + * @param {URL | string} resource * @param {RequestInit} [opts] */ export function initial_fetch(resource, opts) { @@ -88,7 +84,7 @@ export function initial_fetch(resource, opts) { /** * Tries to get the response from the cache, if max-age allows it, else does a fetch. - * @param {RequestInfo | URL} resource + * @param {URL | string} resource * @param {string} resolved * @param {RequestInit} [opts] */ @@ -97,7 +93,11 @@ export function subsequent_fetch(resource, resolved, opts) { const selector = build_selector(resource, opts); const cached = cache.get(selector); if (cached) { - if (performance.now() < cached.ttl) { + // https://developer.mozilla.org/en-US/docs/Web/API/Request/cache#value + if ( + performance.now() < cached.ttl && + ['default', 'force-cache', 'only-if-cached', undefined].includes(opts?.cache) + ) { return new Response(cached.body, cached.init); } @@ -110,7 +110,7 @@ export function subsequent_fetch(resource, resolved, opts) { /** * Build the cache key for a given request - * @param {RequestInfo | URL} resource + * @param {URL | RequestInfo} resource * @param {RequestInit} [opts] */ function build_selector(resource, opts) { diff --git a/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.js b/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.js index f17bd7a9f751..a089408dd005 100644 --- a/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.js +++ b/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.js @@ -1,5 +1,14 @@ +let force = false; + +export function _force_next_fetch() { + force = true; +} + /** @type {import('./$types').PageLoad} */ export async function load({ fetch }) { - const resp = await fetch('/load/cache-control/count'); + const resp = await fetch('/load/cache-control/count', { cache: force ? 'no-cache' : 'default' }); + if (force) { + force = false; + } return resp.json(); } diff --git a/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.svelte b/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.svelte index 3e97ca339e15..b4156bf56e29 100644 --- a/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/load/cache-control/+page.svelte @@ -1,14 +1,24 @@
Count is {data.count}
- + + + + diff --git a/packages/kit/test/apps/basics/src/routes/load/cache-control/count/+server.js b/packages/kit/test/apps/basics/src/routes/load/cache-control/count/+server.js index 92865ed5cdea..999b7fd7cd0a 100644 --- a/packages/kit/test/apps/basics/src/routes/load/cache-control/count/+server.js +++ b/packages/kit/test/apps/basics/src/routes/load/cache-control/count/+server.js @@ -5,3 +5,7 @@ export function GET({ setHeaders }) { setHeaders({ 'cache-control': 'public, max-age=4', age: '2' }); return json({ count }); } + +export function POST() { + return new Response(); +} diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index 0d0395b2db95..6a91def2d288 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -552,24 +552,48 @@ test.describe('Load', () => { expect(await page.textContent('p')).toBe('This text comes from the server load function'); }); - test('load does not call fetch if max-age allows it', async ({ page, request }) => { - await request.get('/load/cache-control/reset'); + test.describe.serial('', () => { + test('load does not call fetch if max-age allows it', async ({ page, request }) => { + await request.get('/load/cache-control/reset'); - page.addInitScript(` + page.addInitScript(` window.now = 0; window.performance.now = () => now; `); - await page.goto('/load/cache-control'); - expect(await page.textContent('p')).toBe('Count is 0'); - await page.waitForTimeout(500); - await page.click('button'); - await page.waitForTimeout(500); - expect(await page.textContent('p')).toBe('Count is 0'); + await page.goto('/load/cache-control'); + expect(await page.textContent('p')).toBe('Count is 0'); + await page.waitForTimeout(500); + await page.click('button.default'); + await page.waitForTimeout(500); + expect(await page.textContent('p')).toBe('Count is 0'); - await page.evaluate(() => (window.now = 2500)); - await page.click('button'); - await expect(page.locator('p')).toHaveText('Count is 2'); + await page.evaluate(() => (window.now = 2500)); + await page.click('button.default'); + await expect(page.locator('p')).toHaveText('Count is 2'); + }); + + test('load does ignore ttl if fetch cache options says so', async ({ page, request }) => { + await request.get('/load/cache-control/reset'); + + await page.goto('/load/cache-control'); + expect(await page.textContent('p')).toBe('Count is 0'); + await page.waitForTimeout(500); + await page.click('button.force'); + await page.waitForTimeout(500); + expect(await page.textContent('p')).toBe('Count is 1'); + }); + + test('load busts cache if non-GET request to resource is made', async ({ page, request }) => { + await request.get('/load/cache-control/reset'); + + await page.goto('/load/cache-control'); + expect(await page.textContent('p')).toBe('Count is 0'); + await page.waitForTimeout(500); + await page.click('button.bust'); + await page.waitForTimeout(500); + expect(await page.textContent('p')).toBe('Count is 1'); + }); }); test('__data.json has cache-control: private, no-store', async ({ page, clicknav }) => {