Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/increment-cache-response-age.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"miniflare": patch
---

Increment the Age response header when responding from cache.

8 changes: 8 additions & 0 deletions packages/miniflare/src/workers/cache/cache.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface CacheMetadata {
headers: string[][];
status: number;
size: number;
stored?: number;
}

type CacheRouteHandler = RouteHandler<
Expand Down Expand Up @@ -302,6 +303,12 @@ export class CacheObject extends MiniflareDurableObject {
// time we don't do this is when the entry isn't found, or expired, in which
// case, we just threw a `CacheMiss`)
assert(resHeaders !== undefined);
const now = this.timers.now();
const age = parseInt(resHeaders.get("age") || "0", 10);
const cachedDuration = Math.round(
(now - (cached.metadata.stored || now)) / 1000
);
resHeaders.set("Age", String(age + cachedDuration));
resHeaders.set("CF-Cache-Status", "HIT");
resRanges ??= [];

Expand Down Expand Up @@ -356,6 +363,7 @@ export class CacheObject extends MiniflareDurableObject {
headers: Object.entries(headers),
status: res.status,
size,
stored: this.timers.now(),
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

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.

🟡 Cached responses can report an age that is too young for slow or large uploads

The time a cached item was saved is recorded (this.timers.now() at packages/miniflare/src/workers/cache/cache.worker.ts:366) only after the whole body has finished being written, rather than when the item was received, so slow or large responses are later reported as newer than they are.
Impact: Clients can be told a cached response is fresher than it really is, which can make them hold on to stale content longer.

Why the timestamp lands late: metadata promise resolves after the blob write

metadata is a promise created from sizePromise (packages/miniflare/src/workers/cache/cache.worker.ts:355-367). KeyValueStorage.put() first awaits this.#blob.put(entry.value) and only then awaits entry.metadata (packages/miniflare/src/workers/shared/keyvalue.worker.ts:208-223), so this.timers.now() inside the .then() executes after the entire body has been streamed. By contrast, the entry's expiration is computed synchronously at request time (packages/miniflare/src/workers/cache/cache.worker.ts:372), so stored and expiration use different clocks. Capturing the timestamp once, before starting the stream, and reusing it for both would keep them consistent.

Prompt for agents
In packages/miniflare/src/workers/cache/cache.worker.ts, the `put` handler records `stored: this.timers.now()` inside the metadata promise (`sizePromise.then(...)`). Because `KeyValueStorage.put()` awaits the blob write before awaiting the metadata promise (packages/miniflare/src/workers/shared/keyvalue.worker.ts), this timestamp is taken after the whole response body has been streamed, not when the response was received. The `expiration` passed to `storage.put` is computed synchronously at request time, so the two values are based on different clock readings; for large or slow bodies the entry's recorded store time is later than its effective expiry basis, making the Age header computed in `match` too small. Consider capturing a single `now` value before starting the stream and using it for both `stored` and `expiration`.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}));

await this.storage.put({
Expand Down