Skip to content

fix: skip 304 short-circuit when get bypasses cache#140

Merged
Kikobeats merged 2 commits into
masterfrom
cursor/critical-bug-investigation-87a9
Jun 30, 2026
Merged

fix: skip 304 short-circuit when get bypasses cache#140
Kikobeats merged 2 commits into
masterfrom
cursor/critical-bug-investigation-87a9

Conversation

@cursor

@cursor cursor Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Bug and impact

Returning null or undefined from .get() is a documented cache bypass. After #138 fixed the null crash, a follow-up request with a matching If-None-Match header (browser refresh, CDN, API client) still returned 304 with an empty body instead of calling send(). Bypass routes silently lost their response body.

Root cause

When .get() bypasses the cache, memoize returns raw = undefined. The handler falls back to {} for etag computation, producing a deterministic ETag. The 304 branch only checked !forceExpiration && !isModified, so a matching If-None-Match skipped send() even though nothing was cached.

Fix

Only return 304 when memoize produced a stored value (raw is truthy). Cache revalidation after a cleared cache still works because get() re-fetches and returns a truthy raw.

Validation

  • Added regression test: MISS for null data value with If-None-Match
  • Full suite: 28/28 tests pass
  • Manual repro confirmed: bypass + If-None-Match now returns 200 with body instead of empty 304
Open in Web View Automation 

Note

Low Risk
Single guard on the 304 branch in the HTTP cache handler; behavior change is limited to documented cache-bypass paths and is covered by a new test.

Overview
Fixes empty 304 responses on cache-bypass routes when clients send If-None-Match.

When .get() returns null/undefined (documented bypass), memoize does not store a payload, but the handler could still treat a matching ETag as “not modified” and return 304 without calling send(), so refreshes/CDN revalidation got an empty body.

The 304 path now requires hasValue (a real memoized entry) in addition to !forceExpiration and a matching ETag, so bypass requests always fall through to send() with 200 and the body. Normal cached revalidation is unchanged when get() returns a stored object.

Adds regression coverage in test/status.js for null .get() plus If-None-Match.

Reviewed by Cursor Bugbot for commit fa54f4a. Bugbot is set up for automated code reviews on this repo. Configure here.

cursoragent and others added 2 commits June 30, 2026 04:09
Returning null/undefined from .get() is a documented cache bypass. After
#138 stopped the null crash, matching If-None-Match still triggered a 304
with an empty body because the etag was derived from the {} fallback while
send() was never called.

Only return 304 when memoize produced a stored value (raw is truthy).

Co-authored-by: kikohumanbeatbox <kikohumanbeatbox@gmail.com>
The 304 revalidation should fire only when memoize actually had a stored
value. `hasValue` is the semantic flag from @keyvhq/memoize for exactly
that, already destructured at the call site. Gating on `raw` truthiness
worked for the documented bypass case only because compress(undefined)
returns undefined, an implicit coupling; hasValue also stays correct when
.get() returns a falsy primitive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Kikobeats Kikobeats marked this pull request as ready for review June 30, 2026 08:35
@Kikobeats Kikobeats merged commit beb7a8e into master Jun 30, 2026
3 checks passed
@Kikobeats Kikobeats deleted the cursor/critical-bug-investigation-87a9 branch June 30, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants