While reviewing cache-related sugar for defineQueryHandler (#1448), an adversarial design review of handleCacheHeaders (src/utils/cache.ts) surfaced one RFC-violating bug and two design footguns. Findings were re-verified against the current main source.
1. Bug: If-Modified-Since is not ignored when If-None-Match is present
RFC 9110 §13.2.2 requires that when a request carries If-None-Match, If-Modified-Since must be ignored — the ETag comparison alone decides. The current implementation ORs the two checks into cacheMatched independently.
Failure scenario: a resource changes twice within the same second (Last-Modified can't advance — the code truncates milliseconds), or modifiedTime is otherwise coarser than the ETag. A client revalidates with its old ETag + old date:
- ETag check: correctly no match (content changed)
- Date check:
If-Modified-Since >= modifiedTime → "not modified"
- Result:
304 — the client keeps stale content indefinitely, when the correct response was 200 with the new body.
Fix: only consult If-Modified-Since when the request has no If-None-Match header.
2. Footgun: public is force-prepended to Cache-Control with no opt-out
const cacheControls = ["public", ...(opts.cacheControls || [])];
It's documented in the JSDoc, but:
cacheControls: ["private"] produces Cache-Control: public, private — contradictory, and a shared cache (CDN) may take public at face value and cache a personalized/authenticated response.
- There is no way to use this utility without
public.
For a utility people will reach for on authenticated endpoints, defaulting to the dangerous directive with no escape hatch is an API hazard. Since v2 is already a breaking rewrite, this is a good window to drop the implicit default (or at least skip it when the user supplies cacheControls).
3. Enhancement: ETag comparison is strict-only
If-None-Match is defined to use weak comparison (RFC 9110 §8.8.3.2), and * must match any current representation. The current per-token exact string match means W/"abc" from a client/proxy never matches a server ETag of "abc", and If-None-Match: * is not handled. Consequence is only missed 304s (wasted bandwidth), not wrong content — mildest of the three.
Severity
- Precedence bug: correctness issue, worth fixing regardless.
- Forced
public: API design decision, ideally settled within the v2 window.
- Weak/
* matching: small enhancement.
Happy to follow up with a PR.
Disclaimer
🤖 This issue was written by Claude Code (analysis reviewed and directed by a human).
While reviewing cache-related sugar for
defineQueryHandler(#1448), an adversarial design review ofhandleCacheHeaders(src/utils/cache.ts) surfaced one RFC-violating bug and two design footguns. Findings were re-verified against the currentmainsource.1. Bug:
If-Modified-Sinceis not ignored whenIf-None-Matchis presentRFC 9110 §13.2.2 requires that when a request carries
If-None-Match,If-Modified-Sincemust be ignored — the ETag comparison alone decides. The current implementation ORs the two checks intocacheMatchedindependently.Failure scenario: a resource changes twice within the same second (
Last-Modifiedcan't advance — the code truncates milliseconds), ormodifiedTimeis otherwise coarser than the ETag. A client revalidates with its old ETag + old date:If-Modified-Since >= modifiedTime→ "not modified"304— the client keeps stale content indefinitely, when the correct response was200with the new body.Fix: only consult
If-Modified-Sincewhen the request has noIf-None-Matchheader.2. Footgun:
publicis force-prepended toCache-Controlwith no opt-outIt's documented in the JSDoc, but:
cacheControls: ["private"]producesCache-Control: public, private— contradictory, and a shared cache (CDN) may takepublicat face value and cache a personalized/authenticated response.public.For a utility people will reach for on authenticated endpoints, defaulting to the dangerous directive with no escape hatch is an API hazard. Since v2 is already a breaking rewrite, this is a good window to drop the implicit default (or at least skip it when the user supplies
cacheControls).3. Enhancement: ETag comparison is strict-only
If-None-Matchis defined to use weak comparison (RFC 9110 §8.8.3.2), and*must match any current representation. The current per-token exact string match meansW/"abc"from a client/proxy never matches a server ETag of"abc", andIf-None-Match: *is not handled. Consequence is only missed304s (wasted bandwidth), not wrong content — mildest of the three.Severity
public: API design decision, ideally settled within the v2 window.*matching: small enhancement.Happy to follow up with a PR.
Disclaimer
🤖 This issue was written by Claude Code (analysis reviewed and directed by a human).