feat(caching): Vary-aware variant caching and conditional-request coalescing safety (v2.2.0) - #2
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Varyheader are now stored under a secondary cache key derived from the request's values for the Vary fields, with a smallIsVaryMarkerentry 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-Encodingresource 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.MemoryCacheStoreandDistributedCacheStore(marker + variants serialize like any other entry).Vary: *remains uncacheable.WriteEntryAsync, so revalidation refreshes the variant key (and the marker), not the primary key.#2 — Conditional requests no longer coalesce with non-conditional ones
RequestKeynow 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
GETand anIf-None-Matchrevalidation for the same URL could collapse into one execution, handing a caller that never sent a validator a bodyless304it can't interpret (most exposed underAddCoalescingOnly). 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.IsVaryMarkeradded (+ 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 withCoalesceKeyHeaders.Tests
VaryVariantCachingTests— variant coexistence (theen → es → enthird-request-is-a-hit regression), cross-contamination isolation, revalidation write-back to the variant key, distributed-store round-trip,Vary: *.ConditionalRequestCoalescingTests— key discrimination (conditional vs plain, same vs different validator,If-Modified-Since, alongsideCoalesceKeyHeaders) + a concurrency test proving a non-conditional caller never receives a304.Verification
-warnaserrorclean onnet8.0andnet10.0; new test files add zero warnings.🤖 Generated with Claude Code