From fddc4b68c4fef0e16ca76dd8f2a2820a8af02355 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 10 May 2026 08:13:42 +0100 Subject: [PATCH 1/4] fix(astro): restore route cache shim --- .changeset/astro-cache-disabled-shim.md | 7 +++++++ packages/astro/src/core/routing/handler.ts | 15 +++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .changeset/astro-cache-disabled-shim.md diff --git a/.changeset/astro-cache-disabled-shim.md b/.changeset/astro-cache-disabled-shim.md new file mode 100644 index 000000000000..7cdab25004b5 --- /dev/null +++ b/.changeset/astro-cache-disabled-shim.md @@ -0,0 +1,7 @@ +--- +'astro': patch +--- + +Fixes a regression where `Astro.cache` was `undefined` when `experimental.cache` was not configured. + +The previous documented behavior is for `Astro.cache` to always be defined as a no-op shim: `cache.set()` warns once, `cache.invalidate()` throws and `cache.enabled` can be used to gate. This allows library and user code can call cache methods without conditional checks. The cache provider registration was being gated at the call site on `experimental.cache` being configured, which meant the disabled shim branch inside the provider was unreachable and the `Astro.cache` getter was never attached to the context. diff --git a/packages/astro/src/core/routing/handler.ts b/packages/astro/src/core/routing/handler.ts index cd0acaeec6a6..dd8a5a3e2e41 100644 --- a/packages/astro/src/core/routing/handler.ts +++ b/packages/astro/src/core/routing/handler.ts @@ -105,14 +105,13 @@ export class AstroHandler { let response; try { - // Only call provider functions when the feature is configured. - // Each call does property lookups + state.provide() which - // allocates a Map on the hot path when nothing is configured. - if (this.#hasSession || this.#app.pipeline.cacheConfig) { - const sessionP = this.#hasSession ? provideSession(state) : undefined; - const cacheP = this.#app.pipeline.cacheConfig ? provideCache(state) : undefined; - if (sessionP || cacheP) await Promise.all([sessionP, cacheP]); - } + // `provideCache` always runs so `Astro.cache` is defined even + // when caching is disabled — it registers a no-op shim that + // warns once on use. `provideSession` is gated because there + // is no equivalent disabled-shim contract for sessions. + const sessionP = this.#hasSession ? provideSession(state) : undefined; + const cacheP = provideCache(state); + if (sessionP || cacheP) await Promise.all([sessionP, cacheP]); // Track feature usage even when skipped. state.pipeline.usedFeatures |= PipelineFeatures.sessions; From 5b7c7484d9cee0c287ffb5264e78a8b00d304ab3 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 10 May 2026 08:18:25 +0100 Subject: [PATCH 2/4] Add test --- packages/astro/test/cache-route.test.ts | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/astro/test/cache-route.test.ts b/packages/astro/test/cache-route.test.ts index 34006c5438c0..93d60b87e7a5 100644 --- a/packages/astro/test/cache-route.test.ts +++ b/packages/astro/test/cache-route.test.ts @@ -155,4 +155,44 @@ describe('context.cache', () => { assert.deepEqual(body, { ok: true }); }); }); + + describe('Disabled (no cache provider configured)', () => { + let fixture: Fixture; + let app: App; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/cache-route/', + output: 'server', + adapter: testAdapter(), + outDir: './dist/cache-route-disabled/', + }); + await fixture.build({}); + app = await fixture.loadTestAdapterApp(); + }); + + async function fetchResponse(path: string) { + const request = new Request('http://example.com' + path); + return app.render(request); + } + + // Regression: Astro.cache must always be defined as a no-op shim + // even when experimental.cache is not configured, so that + // `Astro.cache.set(...)` calls do not crash. + it('Astro.cache.set() is a no-op on .astro pages', async () => { + const response = await fetchResponse('/'); + assert.equal(response.status, 200); + assert.equal(response.headers.get('CDN-Cache-Control'), null); + assert.equal(response.headers.get('Cache-Tag'), null); + }); + + it('context.cache.set() is a no-op in API routes', async () => { + const response = await fetchResponse('/api'); + assert.equal(response.status, 200); + assert.equal(response.headers.get('CDN-Cache-Control'), null); + assert.equal(response.headers.get('Cache-Tag'), null); + const body = await response.json(); + assert.deepEqual(body, { ok: true }); + }); + }); }); From f5fc9f99e97d6eb75a8d579b5e5cc649ed86c4b2 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 10 May 2026 11:09:08 +0100 Subject: [PATCH 3/4] debug: log fetchWithRedirects response status Temporary debug commit to surface what Netlify is returning for the failing test fixture URL in CI. --- .../src/assets/utils/redirectValidation.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/assets/utils/redirectValidation.ts b/packages/astro/src/assets/utils/redirectValidation.ts index ab0d9cd816da..4412dfeaedfd 100644 --- a/packages/astro/src/assets/utils/redirectValidation.ts +++ b/packages/astro/src/assets/utils/redirectValidation.ts @@ -79,7 +79,22 @@ export async function fetchWithRedirects(options: FetchRedirectOptions): Promise const urlString = typeof url === 'string' ? url : url.toString(); const req = new Request(url, { headers }); - const res = await fetchFn(req, { redirect: 'manual' }); + let res: Response; + try { + res = await fetchFn(req, { redirect: 'manual' }); + } catch (err) { + console.error('[debug:fetchWithRedirects] fetch threw for', urlString, err); + throw err; + } + console.error( + '[debug:fetchWithRedirects]', + urlString, + '→', + res.status, + res.headers.get('content-type'), + res.headers.get('location') ?? '', + res.headers.get('server') ?? '', + ); // Handle redirects (301, 302, 303, 307, 308 are actual redirects, not 304 Not Modified) if ([301, 302, 303, 307, 308].includes(res.status)) { From b3e1b15e0123e92821e66134cad96a9a07e8fb03 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 10 May 2026 11:36:12 +0100 Subject: [PATCH 4/4] Revert "debug: log fetchWithRedirects response status" This reverts commit f5fc9f99e97d6eb75a8d579b5e5cc649ed86c4b2. --- .../src/assets/utils/redirectValidation.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/astro/src/assets/utils/redirectValidation.ts b/packages/astro/src/assets/utils/redirectValidation.ts index 4412dfeaedfd..ab0d9cd816da 100644 --- a/packages/astro/src/assets/utils/redirectValidation.ts +++ b/packages/astro/src/assets/utils/redirectValidation.ts @@ -79,22 +79,7 @@ export async function fetchWithRedirects(options: FetchRedirectOptions): Promise const urlString = typeof url === 'string' ? url : url.toString(); const req = new Request(url, { headers }); - let res: Response; - try { - res = await fetchFn(req, { redirect: 'manual' }); - } catch (err) { - console.error('[debug:fetchWithRedirects] fetch threw for', urlString, err); - throw err; - } - console.error( - '[debug:fetchWithRedirects]', - urlString, - '→', - res.status, - res.headers.get('content-type'), - res.headers.get('location') ?? '', - res.headers.get('server') ?? '', - ); + const res = await fetchFn(req, { redirect: 'manual' }); // Handle redirects (301, 302, 303, 307, 308 are actual redirects, not 304 Not Modified) if ([301, 302, 303, 307, 308].includes(res.status)) {