Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e6d5b28
feat(cache): add in-memory tier, size limits, hex sharding, and obser…
vibegui Mar 18, 2026
8c4e5d1
feat(cache): add comprehensive cache observability metrics
vibegui Mar 18, 2026
9e3c534
fix(cache): preserve response status, validate env vars, fix metric o…
vibegui Mar 18, 2026
7ba9241
fix(cache): address code review findings
vibegui Mar 18, 2026
0010914
fix(cache): address correctness issues in env parsing, shard init, LR…
vibegui Mar 18, 2026
23b5ce1
feat(cache): lazy re-index disk entries into LRU on pod restart
vibegui Mar 18, 2026
b3696ff
perf(cache): eliminate hot-path allocations and fix correctness bugs
vibegui Mar 18, 2026
60d5f50
fix(cache): async mkdir and single-tier cache-tier header
vibegui Mar 18, 2026
ad86a9b
fix(cache): reject oversized entries before writing to disk
vibegui Mar 18, 2026
7b2e290
test(cache): add tests for tiered cache, in-memory cache, and lazy re…
vibegui Mar 18, 2026
9ed8346
fix(cache): evict stale disk entry when oversized write is rejected
vibegui Mar 18, 2026
464e1f0
fix(cache): NaN guard, oversized check in L1, memoize LRU open()
vibegui Mar 18, 2026
093066e
perf(cache): skip body read in L1 put when Content-Length exceeds limit
vibegui Mar 18, 2026
ff09a48
feat(cache): extend default stale window to 1h, add STALE_WINDOW_S en…
vibegui Mar 18, 2026
84560bf
fix(cache): bump STALE_TTL_PERIOD default from 30s to 1h
vibegui Mar 18, 2026
2c062bd
feat(cache): bot write guard, L1 admission filter, eviction logging
vibegui Mar 18, 2026
ac7edcd
fix(cache): update inMemoryCache tests for admission filter
vibegui Mar 18, 2026
7f6bbac
feat(cache): write rate warning and disk fill warning
vibegui Mar 18, 2026
9f84eee
fix(cache): separate singleFlight key for bot requests
vibegui Mar 18, 2026
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
72 changes: 68 additions & 4 deletions blocks/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ const stats = {
unit: "ms",
valueType: ValueType.DOUBLE,
}),
cacheEntrySize: meter.createHistogram("loader_cache_entry_size", {
description: "size of cached loader responses in bytes",
unit: "bytes",
valueType: ValueType.DOUBLE,
}),
bgRevalidation: meter.createHistogram("loader_bg_revalidation", {
description: "duration of background stale-while-revalidate calls",
unit: "ms",
valueType: ValueType.DOUBLE,
}),
};

let maybeCache: Cache | undefined;
Expand All @@ -155,6 +165,9 @@ caches?.open("loader")
.catch(() => maybeCache = undefined);

const MAX_AGE_S = parseInt(Deno.env.get("CACHE_MAX_AGE_S") ?? "60"); // 60 seconds
const CACHE_MAX_ENTRY_SIZE = parseInt(
Deno.env.get("CACHE_MAX_ENTRY_SIZE") ?? "2097152", // 2 MB
) || 2097152;

// Reuse TextEncoder instance to avoid repeated instantiation
const textEncoder = new TextEncoder();
Expand Down Expand Up @@ -248,7 +261,14 @@ const wrapLoader = (
!shouldNotCache && ctx.vary?.push(cacheKeyValue);

status = "bypass";
stats.cache.add(1, { status, loader });
const bypassReason = isCacheNoStore
? "no-store"
: isCacheNoCache
? "no-cache"
: isCacheKeyNull
? "null-key"
: "disabled";
stats.cache.add(1, { status, loader, reason: bypassReason });

RequestContext?.signal?.throwIfAborted();
return await handler(props, req, ctx);
Expand Down Expand Up @@ -297,6 +317,19 @@ const wrapLoader = (
// Serialize and encode once on the main thread.
const jsonStringEncoded = textEncoder.encode(JSON.stringify(json));

// Skip caching oversized entries to protect disk and memory.
// Also evict any existing stale entry so it doesn't stay pinned forever.
if (jsonStringEncoded.length > CACHE_MAX_ENTRY_SIZE) {
cache.delete(request).catch((error) =>
logger.error(`loader error ${error}`)
);
return json;
}

if (OTEL_ENABLE_EXTRA_METRICS) {
stats.cacheEntrySize.record(jsonStringEncoded.length, { loader });
}

const expires = new Date(Date.now() + (cacheMaxAge * 1e3))
.toUTCString();
const headerPairs: [string, string][] = [
Expand All @@ -305,7 +338,7 @@ const wrapLoader = (
["Content-Length", "" + jsonStringEncoded.length],
];

// Cache write goes through the full chain (LRU → filesystem)
// Cache write goes through the full chain (LRU → in-memory → filesystem)
// so the LRU registers the key for fast match lookups.
// The filesystem layer offloads the actual I/O to a worker thread
// when DECO_CACHE_WRITE_WORKER=true.
Expand Down Expand Up @@ -336,13 +369,44 @@ const wrapLoader = (
status = "stale";
stats.cache.add(1, { status, loader });

bgFlights.do(request.url, callHandlerAndCache)
.catch((error) => logger.error(`loader error ${error}`));
// Timer lives inside the singleFlight fn so it records exactly once
// per revalidation, not once per concurrent waiter on the same key.
bgFlights.do(request.url, async () => {
const bgStart = performance.now();
try {
return await callHandlerAndCache();
} finally {
if (OTEL_ENABLE_EXTRA_METRICS) {
stats.bgRevalidation.record(
performance.now() - bgStart,
{ loader },
);
}
}
}).catch((error) => logger.error(`loader error ${error}`));
} else {
status = "hit";
stats.cache.add(1, { status, loader });
}

if (OTEL_ENABLE_EXTRA_METRICS) {
const cl = parseInt(
matched.headers.get("Content-Length") ?? "0",
);
if (cl > 0) {
stats.cacheEntrySize.record(cl, { loader, status });
}
}

if (OTEL_ENABLE_EXTRA_METRICS) {
const parseStart = performance.now();
const result = await matched.json();
stats.latency.record(performance.now() - parseStart, {
loader,
status: "json_parse",
});
return result;
}
return await matched.json();
};

Expand Down
18 changes: 16 additions & 2 deletions runtime/caches/cacheWriteWorker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Worker thread for cache write operations.
// Offloads SHA1 hashing, buffer combining, and FS writes from the main event loop.

const CACHE_MAX_ENTRY_SIZE = parseInt(
Deno.env.get("CACHE_MAX_ENTRY_SIZE") ?? "2097152", // 2 MB
) || 2097152;

@cubic-dev-ai cubic-dev-ai Bot Mar 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The || 2097152 fallback makes an explicit CACHE_MAX_ENTRY_SIZE=0 impossible, because 0 is coerced to the default 2MB.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At runtime/caches/cacheWriteWorker.ts, line 6:

<comment>The `|| 2097152` fallback makes an explicit `CACHE_MAX_ENTRY_SIZE=0` impossible, because `0` is coerced to the default 2MB.</comment>

<file context>
@@ -1,6 +1,10 @@
 
+const CACHE_MAX_ENTRY_SIZE = parseInt(
+  Deno.env.get("CACHE_MAX_ENTRY_SIZE") ?? "2097152", // 2 MB
+) || 2097152;
+
 const textEncoder = new TextEncoder();
</file context>
Fix with Cubic


const textEncoder = new TextEncoder();

const initializedDirs = new Set<string>();
Expand Down Expand Up @@ -57,6 +61,12 @@ function generateCombinedBuffer(
return buf;
}

function shardedPath(cacheDir: string, key: string): string {
const l1 = key.substring(0, 2);
const l2 = key.substring(2, 4);
return `${cacheDir}/${l1}/${l2}/${key}`;
}

// --- Message handler ---

export interface CacheWriteMessage {
Expand Down Expand Up @@ -85,8 +95,12 @@ self.onmessage = async (e: MessageEvent<CacheWriteMessage>) => {
// Combine into single buffer
const buffer = generateCombinedBuffer(body, headersBytes);

// Write to filesystem
const filePath = `${cacheDir}/${cacheKey}`;
if (buffer.length > CACHE_MAX_ENTRY_SIZE) return;

// Write to filesystem (with hex sharding for directory distribution)
const filePath = shardedPath(cacheDir, cacheKey);
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
ensureCacheDir(dir);
await Deno.writeFile(filePath, buffer);
} catch (err) {
console.error("[cache-write-worker] error:", err);
Expand Down
9 changes: 9 additions & 0 deletions runtime/caches/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ export const withInstrumentation = (
const result = getCacheStatus(isMatch);

span.setAttribute("cache_status", result);
if (isMatch) {
const cl = isMatch.headers.get("Content-Length");
if (cl) span.setAttribute("content_length", parseInt(cl));
const tier = isMatch.headers.get("X-Cache-Tier");
if (tier) {
span.setAttribute("cache_tier", parseInt(tier));
isMatch.headers.delete("X-Cache-Tier");
}
}
cacheHit.add(1, {
result,
engine,
Expand Down
35 changes: 30 additions & 5 deletions runtime/caches/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ import {
const FILE_SYSTEM_CACHE_DIRECTORY =
Deno.env.get("FILE_SYSTEM_CACHE_DIRECTORY") ?? "/tmp/deco_cache";

const CACHE_MAX_ENTRY_SIZE = parseInt(
Deno.env.get("CACHE_MAX_ENTRY_SIZE") ?? "2097152", // 2 MB
);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent size validation: putFile bypasses the size limit.

The CACHE_MAX_ENTRY_SIZE is only enforced on reads (line 157-159), not on writes. Both the inline putFile path and the worker path (see cacheWriteWorker.ts lines 78-102) write entries to disk without size validation. This causes wasted I/O: oversized responses are written, then silently deleted on the first read.

Consider validating size in putFile and rejecting oversized entries before the write:

Suggested fix for inline path
   async function putFile(
     key: string,
     responseArray: Uint8Array,
   ) {
+    if (responseArray.length > CACHE_MAX_ENTRY_SIZE) {
+      return; // Skip caching oversized entries
+    }
     if (!isCacheInitialized) {
       await assertCacheDirectory();
     }

The worker path in cacheWriteWorker.ts should also include this validation before calling Deno.writeFile.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runtime/caches/fileSystem.ts` around lines 14 - 16, putFile currently writes
entries without checking CACHE_MAX_ENTRY_SIZE so oversized blobs are written
then deleted on read; add a pre-write size validation in putFile to compare the
byte length of the data against CACHE_MAX_ENTRY_SIZE and reject/return early for
oversized entries (do not call Deno.writeFile), and mirror the same check inside
the cacheWriteWorker's write-handling path (the code that ultimately calls
Deno.writeFile in cacheWriteWorker.ts) so the worker rejects the job before
performing any I/O; ensure both places surface a clear rejection (throw/return
error or skip enqueue) and avoid writing files that exceed CACHE_MAX_ENTRY_SIZE.


const initializedShardDirs = new Set<string>();

function shardedPath(cacheDir: string, key: string): string {
const l1 = key.substring(0, 2);
const l2 = key.substring(2, 4);
return `${cacheDir}/${l1}/${l2}/${key}`;
}

// Reuse TextEncoder instance to avoid repeated instantiation
const textEncoder = new TextEncoder();

Expand Down Expand Up @@ -106,7 +118,7 @@ function createFileSystemCache(): CacheStorage {
if (
FILE_SYSTEM_CACHE_DIRECTORY && !existsSync(FILE_SYSTEM_CACHE_DIRECTORY)
) {
await Deno.mkdirSync(FILE_SYSTEM_CACHE_DIRECTORY, { recursive: true });
await Deno.mkdir(FILE_SYSTEM_CACHE_DIRECTORY, { recursive: true });
}
isCacheInitialized = true;
} catch (err) {
Expand All @@ -118,11 +130,20 @@ function createFileSystemCache(): CacheStorage {
key: string,
responseArray: Uint8Array,
) {
if (responseArray.length > CACHE_MAX_ENTRY_SIZE) return;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
if (!isCacheInitialized) {
await assertCacheDirectory();
}
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;

const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
if (!initializedShardDirs.has(dir)) {
try {
await Deno.mkdir(dir, { recursive: true });
initializedShardDirs.add(dir);
} catch {
// transient failure — don't mark initialized so next write retries mkdir
}
}
await Deno.writeFile(filePath, responseArray);
return;
}
Expand All @@ -132,8 +153,12 @@ function createFileSystemCache(): CacheStorage {
await assertCacheDirectory();
}
try {
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);
const fileContent = await Deno.readFile(filePath);
if (fileContent.length > CACHE_MAX_ENTRY_SIZE) {
Deno.remove(filePath).catch(() => {});
return null;
}
return fileContent;
} catch (_err) {
const err = _err as { code?: string };
Expand All @@ -151,7 +176,7 @@ function createFileSystemCache(): CacheStorage {
await assertCacheDirectory();
}
try {
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
const filePath = shardedPath(FILE_SYSTEM_CACHE_DIRECTORY, key);
await Deno.remove(filePath);
return true;
} catch (err) {
Expand Down
85 changes: 85 additions & 0 deletions runtime/caches/inMemoryCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { LRUCache } from "npm:lru-cache@10.2.0";
import {
assertCanBeCached,
assertNoOptions,
baseCache,
withCacheNamespace,
} from "./utils.ts";

const MEMORY_CACHE_MAX_SIZE = parseInt(
Deno.env.get("MEMORY_CACHE_MAX_SIZE") ?? "268435456", // 256 MB
);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
const MEMORY_CACHE_MAX_ITEMS = parseInt(
Deno.env.get("MEMORY_CACHE_MAX_ITEMS") ?? "2048",
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

interface CacheEntry {
body: Uint8Array;
headers: [string, string][];
status: number;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function createInMemoryCache(): CacheStorage {
const store = new LRUCache<string, CacheEntry>({
max: MEMORY_CACHE_MAX_ITEMS,
maxSize: MEMORY_CACHE_MAX_SIZE,
sizeCalculation: (entry) => entry.body.length,
});

const caches: CacheStorage = {
delete: () => {
throw new Error("Not Implemented");
},
has: () => {
throw new Error("Not Implemented");
},
keys: () => {
throw new Error("Not Implemented");
},
match: () => {
throw new Error("Not Implemented");
},
open: (cacheName: string): Promise<Cache> => {
const requestURLSHA1 = withCacheNamespace(cacheName);
return Promise.resolve({
...baseCache,
delete: async (
request: RequestInfo | URL,
_options?: CacheQueryOptions,
): Promise<boolean> => {
Comment on lines +91 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

For the Cache API, what is the expected behavior of Cache.delete(request, options)regardingCacheQueryOptionssuch asignoreSearchandignoreMethod?

💡 Result:

The Cache.delete(request, options) method uses the provided CacheQueryOptions (such as ignoreSearch and ignoreMethod) to control how the matching is performed when identifying the cache entry to delete. It finds the Cache entry whose key matches the request according to the matching rules modified by the options, deletes it if found (resolving to true), or resolves to false if no match is found. Specifically: - ignoreSearch: When true, ignores the query string in the URL during matching (defaults to false). - ignoreMethod: When true, does not validate the Request's HTTP method (normally only GET/HEAD allowed; defaults to false). This behavior is identical to how options work in Cache.match and Cache.matchAll.

Citations:


🏁 Script executed:

# First, let's find and examine the inMemoryCache.ts file
find . -name "inMemoryCache.ts" -type f

Repository: deco-cx/deco

Length of output: 89


🏁 Script executed:

# Once found, let's examine the full delete() and match() implementations
cat -n runtime/caches/inMemoryCache.ts | head -100

Repository: deco-cx/deco

Length of output: 3189


🏁 Script executed:

# Search for assertNoOptions usage in the codebase to understand the pattern
rg "assertNoOptions" --type typescript

Repository: deco-cx/deco

Length of output: 90


🏁 Script executed:

# Let's check the utils.ts file to see what assertNoOptions does
cat -n runtime/caches/utils.ts

Repository: deco-cx/deco

Length of output: 4551


🏁 Script executed:

# Search for assertNoOptions usage with context
rg "assertNoOptions" -A 2 -B 2 --type ts

Repository: deco-cx/deco

Length of output: 4070


Add assertion for unsupported CacheQueryOptions in delete() to match match() behavior.

The delete() method accepts CacheQueryOptions but silently ignores them (parameter has underscore prefix), while match() explicitly rejects them via assertNoOptions(). This inconsistency can hide behavior mismatches when callers pass ignoreSearch, ignoreMethod, or ignoreVary. Other cache implementations (e.g., redis.ts) already call assertNoOptions() in their delete() methods—align this implementation with that pattern.

Suggested fix
         delete: async (
           request: RequestInfo | URL,
-          _options?: CacheQueryOptions,
+          options?: CacheQueryOptions,
         ): Promise<boolean> => {
+          assertNoOptions(options);
           const cacheKey = await requestURLSHA1(request);
           return store.delete(cacheKey);
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
delete: async (
request: RequestInfo | URL,
_options?: CacheQueryOptions,
): Promise<boolean> => {
delete: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<boolean> => {
assertNoOptions(options);
const cacheKey = await requestURLSHA1(request);
return store.delete(cacheKey);
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runtime/caches/inMemoryCache.ts` around lines 46 - 49, The delete() method in
inMemoryCache.ts currently ignores its CacheQueryOptions parameter (named
_options) unlike match() which calls assertNoOptions(); update delete to
assertNoOptions on the options to reject unsupported flags. Inside the async
delete implementation (function named delete), call assertNoOptions(_options,
'delete') at the start (or rename to options and call assertNoOptions(options,
'delete')) so it behaves like redis.ts and match() and consistently rejects
ignoreSearch/ignoreMethod/ignoreVary.

const cacheKey = await requestURLSHA1(request);
return store.delete(cacheKey);
},
match: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<Response | undefined> => {
assertNoOptions(options);
const cacheKey = await requestURLSHA1(request);
const entry = store.get(cacheKey);
if (!entry) return undefined;
return new Response(entry.body as unknown as BodyInit, {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
headers: new Headers(entry.headers),
status: entry.status,
});
},
put: async (
request: RequestInfo | URL,
response: Response,
): Promise<void> => {
const req = new Request(request);
assertCanBeCached(req, response);
if (!response.body) return;
const cacheKey = await requestURLSHA1(request);
const body = new Uint8Array(await response.arrayBuffer());
const headers: [string, string][] = [...response.headers.entries()];
Comment on lines +118 to +136

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the Fetch API, does Response.arrayBuffer() return an empty ArrayBuffer when the response body is null, and is caching such responses valid behavior?

💡 Result:

Yes, Response.arrayBuffer returns an empty ArrayBuffer (with byteLength 0) when the response body is null. Caching such responses is valid behavior. In the Fetch API, Response.body is null for responses constructed with null body (e.g., new Response(null)) or per HTTP semantics for no-body statuses like 204 No Content, 205 Reset Content, and 304 Not Modified. However, browsers do not implement the spec's null body exactly; they provide an empty ReadableStream instead. The arrayBuffer method reads the body stream to completion, resolving to an empty ArrayBuffer for null or empty bodies. This is confirmed by MDN documentation stating arrayBuffer resolves with an ArrayBuffer after reading the stream, and practical observations in issues where null bodies become empty ArrayBuffers (e.g., Safari workaround turning null to empty ArrayBuffer). The WHATWG Fetch spec links to body methods like arrayBuffer, and implementations copy the buffer source, yielding empty for no content. Caching Responses with null/empty bodies is valid and common, e.g., in Service Workers via caches.match returning cached empty-body responses (e.g., 304), as shown in WHATWG discussions and examples.

Citations:


🏁 Script executed:

# Find the file and check its structure
fd -t f "inMemoryCache.ts" runtime/caches/

Repository: deco-cx/deco

Length of output: 87


🏁 Script executed:

# View the specific section around lines 72-75
cat -n runtime/caches/inMemoryCache.ts | sed -n '60,90p'

Repository: deco-cx/deco

Length of output: 1077


🏁 Script executed:

# Find where assertCanBeCached is defined and what it does
rg "assertCanBeCached" -A 5

Repository: deco-cx/deco

Length of output: 3531


🏁 Script executed:

# Check if there are any tests related to empty body caching
rg -i "empty.*body|body.*empty|arrayBuffer.*null" --type ts --type js -A 3

Repository: deco-cx/deco

Length of output: 224


🏁 Script executed:

# Look for any usage of the cache to understand if empty responses are expected
rg "store\.get|store\.set" runtime/caches/ -B 2 -A 2

Repository: deco-cx/deco

Length of output: 868


🏁 Script executed:

# Look for tests related to caching empty bodies or null bodies
find . -type f \( -name "*test*" -o -name "*.test.*" -o -name "*.spec.*" \) | xargs rg -i "cache.*empty|empty.*cache|null.*body|body.*null" 2>/dev/null | head -30

Repository: deco-cx/deco

Length of output: 243


🏁 Script executed:

# Check if there are any comments explaining why the guard exists
rg -B 3 "if.*!response\.body" runtime/caches/

Repository: deco-cx/deco

Length of output: 1267


🏁 Script executed:

# Look for any documentation or comments in the utils file about caching rules
cat -n runtime/caches/utils.ts | head -50

Repository: deco-cx/deco

Length of output: 1591


Remove the early return guard that prevents caching of responses with null/empty bodies.

Line 72 returns early when response.body is null, preventing valid responses from being cached. Per the Fetch API, Response.arrayBuffer() returns an empty ArrayBuffer for null bodies, and such responses are cacheable (e.g., 304 Not Modified, 204 No Content). This pattern exists across all cache implementations (inMemoryCache, redis, fileSystem, headerscache, lrucache) and should be removed from all of them.

Suggested fix
           const req = new Request(request);
           assertCanBeCached(req, response);
-          if (!response.body) return;
           const cacheKey = await requestURLSHA1(request);
           const body = new Uint8Array(await response.arrayBuffer());
           const headers: [string, string][] = [...response.headers.entries()];
           store.set(cacheKey, { body, headers, status: response.status });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response.body) return;
const cacheKey = await requestURLSHA1(request);
const body = new Uint8Array(await response.arrayBuffer());
const headers: [string, string][] = [...response.headers.entries()];
const req = new Request(request);
assertCanBeCached(req, response);
const cacheKey = await requestURLSHA1(request);
const body = new Uint8Array(await response.arrayBuffer());
const headers: [string, string][] = [...response.headers.entries()];
store.set(cacheKey, { body, headers, status: response.status });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@runtime/caches/inMemoryCache.ts` around lines 72 - 75, Remove the early
return that checks response.body (the line "if (!response.body) return;") so
responses with null/empty bodies are still cached; keep using
requestURLSHA1(request) for the cache key and continue to read the body via
response.arrayBuffer() (which yields an empty ArrayBuffer for null bodies) and
collect headers via [...response.headers.entries()] as before; apply the same
removal of the guard across inMemoryCache, redis, fileSystem, headerscache, and
lrucache implementations to ensure 204/304 and other empty-body responses are
cached.

store.set(cacheKey, { body, headers, status: response.status });
},
});
},
};

return caches;
}

export const caches = createInMemoryCache();
Loading
Loading