diff --git a/src/lib/response-cache.ts b/src/lib/response-cache.ts index 13a982073..6f3b67440 100644 --- a/src/lib/response-cache.ts +++ b/src/lib/response-cache.ts @@ -307,18 +307,20 @@ function buildResponseHeaders( // Cache bypass control // --------------------------------------------------------------------------- -let cacheDisabledFlag = false; +let cacheReadBypassed = false; /** - * Disable the response cache for the current process. - * Called when `--fresh` flag is passed to a command. + * Bypass cache reads for the current process. + * + * Called when `--fresh` flag is passed to a command. Fresh API responses are + * still written to cache so subsequent invocations serve updated data. */ export function disableResponseCache(): void { - cacheDisabledFlag = true; + cacheReadBypassed = true; } /** - * Re-enable the response cache after `disableResponseCache()` was called. + * Re-enable cache reads after `disableResponseCache()` was called. * * This is only needed in tests to prevent one test's `--fresh` flag from * permanently disabling caching for subsequent tests in the same process. @@ -327,17 +329,28 @@ export function disableResponseCache(): void { * @internal Exported for testing */ export function resetCacheState(): void { - cacheDisabledFlag = false; + cacheReadBypassed = false; } /** - * Check if response caching is disabled. - * Cache is disabled when: - * - `disableResponseCache()` was called (--refresh flag) + * Check if cache reads are disabled. + * Reads are skipped when: + * - `disableResponseCache()` was called (`--fresh` flag) * - `SENTRY_NO_CACHE=1` environment variable is set */ export function isCacheDisabled(): boolean { - return cacheDisabledFlag || getEnv().SENTRY_NO_CACHE === "1"; + return cacheReadBypassed || getEnv().SENTRY_NO_CACHE === "1"; +} + +/** + * Check if cache writes are disabled. + * + * Only the `SENTRY_NO_CACHE=1` env var disables writes. The `--fresh` flag + * intentionally allows writes so that the freshly-fetched response replaces + * any stale cache entry. + */ +function isCacheWriteDisabled(): boolean { + return getEnv().SENTRY_NO_CACHE === "1"; } // --------------------------------------------------------------------------- @@ -474,7 +487,7 @@ export async function storeCachedResponse( ): Promise { if ( method !== "GET" || - isCacheDisabled() || + isCacheWriteDisabled() || !response.ok || classifyUrl(url) === "no-cache" ) { diff --git a/test/lib/response-cache.test.ts b/test/lib/response-cache.test.ts index b17fbb378..e91e6eead 100644 --- a/test/lib/response-cache.test.ts +++ b/test/lib/response-cache.test.ts @@ -11,6 +11,7 @@ import { join } from "node:path"; import { buildCacheKey, clearResponseCache, + disableResponseCache, getCachedResponse, normalizeUrl, resetCacheState, @@ -262,6 +263,75 @@ describe("cache bypass", () => { const cached = await getCachedResponse(TEST_METHOD, TEST_URL, {}); expect(cached).toBeUndefined(); }); + + test("--fresh bypasses cache reads", async () => { + await storeCachedResponse( + TEST_METHOD, + TEST_URL, + {}, + mockResponse(TEST_BODY) + ); + + disableResponseCache(); + + const cached = await getCachedResponse(TEST_METHOD, TEST_URL, {}); + expect(cached).toBeUndefined(); + }); + + test("--fresh still allows cache writes", async () => { + disableResponseCache(); + + const freshBody = { data: "fresh" }; + await storeCachedResponse( + TEST_METHOD, + TEST_URL, + {}, + mockResponse(freshBody) + ); + + // Re-enable cache reads to verify the write succeeded + resetCacheState(); + + const cached = await getCachedResponse(TEST_METHOD, TEST_URL, {}); + expect(cached).toBeDefined(); + expect(await cached!.json()).toEqual(freshBody); + }); + + test("--fresh round-trip: stale entry is replaced by fresh response", async () => { + const staleBody = { data: "stale" }; + const freshBody = { data: "fresh" }; + + // Store initial stale entry + await storeCachedResponse( + TEST_METHOD, + TEST_URL, + {}, + mockResponse(staleBody) + ); + + // Activate --fresh: reads are bypassed, but writes still go through + disableResponseCache(); + + // Verify stale entry is not served + const duringFresh = await getCachedResponse(TEST_METHOD, TEST_URL, {}); + expect(duringFresh).toBeUndefined(); + + // Store fresh response (overwrites the stale entry) + await storeCachedResponse( + TEST_METHOD, + TEST_URL, + {}, + mockResponse(freshBody) + ); + + // Re-enable cache reads (simulates next invocation without --fresh) + resetCacheState(); + + // Verify fresh data is served from cache + const afterFresh = await getCachedResponse(TEST_METHOD, TEST_URL, {}); + expect(afterFresh).toBeDefined(); + expect(await afterFresh!.json()).toEqual(freshBody); + }); }); // ---------------------------------------------------------------------------