Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/astro-cache-disabled-shim.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 7 additions & 8 deletions packages/astro/src/core/routing/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
40 changes: 40 additions & 0 deletions packages/astro/test/cache-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});
});
Loading