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
35 changes: 24 additions & 11 deletions src/lib/response-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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";
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -474,7 +487,7 @@ export async function storeCachedResponse(
): Promise<void> {
if (
method !== "GET" ||
isCacheDisabled() ||
isCacheWriteDisabled() ||
!response.ok ||
classifyUrl(url) === "no-cache"
) {
Expand Down
70 changes: 70 additions & 0 deletions test/lib/response-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { join } from "node:path";
import {
buildCacheKey,
clearResponseCache,
disableResponseCache,
getCachedResponse,
normalizeUrl,
resetCacheState,
Expand Down Expand Up @@ -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);
});
});

// ---------------------------------------------------------------------------
Expand Down
Loading