Skip to content

handleCacheHeaders: conditional-request precedence bug + forced public Cache-Control #1453

Description

@pi0x

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

  1. Precedence bug: correctness issue, worth fixing regardless.
  2. Forced public: API design decision, ideally settled within the v2 window.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions