Skip to content

feat(caching): Vary-aware variant caching and conditional-request coalescing safety (v2.2.0) - #2

Merged
FranRuiz98 merged 1 commit into
masterfrom
feat/vary-variants-conditional-coalescing
Jul 21, 2026
Merged

feat(caching): Vary-aware variant caching and conditional-request coalescing safety (v2.2.0)#2
FranRuiz98 merged 1 commit into
masterfrom
feat/vary-variants-conditional-coalescing

Conversation

@FranRuiz98

Copy link
Copy Markdown
Owner

What & why

Two adjacent correctness fixes in the caching/coalescing paths, each a self-contained change with its own regression tests.

#1 — Vary: cache multiple representations (RFC 9111 §4.1)

Responses carrying a Vary header are now stored under a secondary cache key derived from the request's values for the Vary fields, with a small IsVaryMarker entry at the primary key recording which headers to vary on. Lookups follow the marker to the matching variant.

Previously only one representation could be cached per URL: a Vary: Accept-Encoding resource requested by a gzip client and then an identity client kept overwriting the single entry, so content-negotiated endpoints never got variant cache hits — every alternation was a full refetch.

  • Works for both MemoryCacheStore and DistributedCacheStore (marker + variants serialize like any other entry).
  • Vary: * remains uncacheable.
  • All writes route through one WriteEntryAsync, so revalidation refreshes the variant key (and the marker), not the primary key.
  • The marker copies the representation's expiry/validator metadata purely so its eviction deadline tracks the variant it points to; it is never served as a response.

#2 — Conditional requests no longer coalesce with non-conditional ones

RequestKey now folds any present conditional request headers (If-None-Match, If-Modified-Since, If-Match, If-Unmodified-Since, If-Range) into the coalescing key.

Previously a plain GET and an If-None-Match revalidation for the same URL could collapse into one execution, handing a caller that never sent a validator a bodyless 304 it can't interpret (most exposed under AddCoalescingOnly). Identical revalidations still share a key, so a revalidation storm is still collapsed into a single origin call. The common non-conditional path keeps its method+URL fast path.

Changes

  • CacheEntry.IsVaryMarker added (+ hand-written JSON converter read/write, covered by round-trip test).
  • CachingMiddleware: ResolveEntryAsync / WriteEntryAsync / BuildVariantKey / CreateVaryMarker; lookup + all three 304-refresh write sites routed through them.
  • RequestKey.Create: conditional-header folding, unioned with CoalesceKeyHeaders.

Tests

  • New VaryVariantCachingTests — variant coexistence (the en → es → en third-request-is-a-hit regression), cross-contamination isolation, revalidation write-back to the variant key, distributed-store round-trip, Vary: *.
  • New ConditionalRequestCoalescingTests — key discrimination (conditional vs plain, same vs different validator, If-Modified-Since, alongside CoalesceKeyHeaders) + a concurrency test proving a non-conditional caller never receives a 304.
  • Each critical test would fail on the old code (Vary: old count 3 vs 2; coalescing: old collapses to 1 call with a wrong response per caller).

Verification

  • Library builds -warnaserror clean on net8.0 and net10.0; new test files add zero warnings.
  • 343 tests pass (330 pre-existing unchanged + 13 new).
  • Version bumped to 2.2.0 (minor — new capability, backward compatible) with a changelog entry. All three existing Vary tests still hold.

🤖 Generated with Claude Code

…lescing safety (v2.2.0)

Two adjacent correctness fixes in the caching/coalescing paths.

#1 Vary: cache multiple representations (RFC 9111 §4.1)
Responses with a Vary header are stored under a secondary cache key derived
from the request's values for the Vary fields, with a small IsVaryMarker
entry at the primary key recording which headers to vary on. Lookups follow
the marker to the matching variant. Previously only one representation could
be cached per URL, so a Vary: Accept-Encoding resource requested by a gzip
client then an identity client kept overwriting the single entry — content-
negotiated endpoints never got variant hits. Works for both MemoryCacheStore
and DistributedCacheStore (marker + variants serialize like any entry).
Vary: * stays uncacheable. All writes route through WriteEntryAsync so
revalidation refreshes the variant key, not the primary.

#2 Conditional requests no longer coalesce with non-conditional ones
RequestKey now folds present conditional headers (If-None-Match,
If-Modified-Since, If-Match, If-Unmodified-Since, If-Range) into the
coalescing key. Previously a plain GET and an If-None-Match revalidation for
the same URL could collapse into one execution, handing a caller that never
sent a validator a bodyless 304. Identical revalidations still share a key,
so a revalidation storm is still collapsed to one origin call. The common
non-conditional path keeps its method+URL fast path.

- Add CacheEntry.IsVaryMarker (+ JSON converter read/write).
- New tests: VaryVariantCachingTests (variant coexistence, isolation,
  revalidation write-back, distributed store, Vary: *) and
  ConditionalRequestCoalescingTests (key discrimination + concurrency proving
  a non-conditional caller never receives a 304). 343 tests pass, zero warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@FranRuiz98
FranRuiz98 merged commit d43e001 into master Jul 21, 2026
2 checks passed
@FranRuiz98
FranRuiz98 deleted the feat/vary-variants-conditional-coalescing branch July 21, 2026 17:02
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.

1 participant