HTTP cache revalidation is a correctness mechanism before it is an optimization. A cache can return a previously stored representation without contacting the origin while that response remains fresh, then use conditional requests such as If-None-Match or If-Modified-Since when validation is required. The acceptance problem is therefore broader than “did the server return 304?” It must prove that the cache selected the right representation, obeyed freshness limits, validated the right validator, and never crossed an authorization or personalization boundary.
The primary authorities for this playbook are RFC 9110: HTTP Semantics (IETF, June 2022; accessed September 22, 2026) and RFC 9111: HTTP Caching (IETF, June 2022; accessed September 22, 2026). Supporting protocol sources are RFC 9211: The Cache-Status HTTP Response Header Field (IETF, June 2022; accessed September 22, 2026) and ,c 39(May 2010; accessed September 22, 2026).
For browser-facing interpretation, this playbook also uses the WHATWG Fetch Living Standard (Living Standard; snapshot accessed September 22, 2026). It does not assume that a browser, CDN, reverse proxy, or managed edge cache behaves identically to the controlled cache used in this lab. Product-specific behavior needs product-specific documentation and evidence.
The laboratory run used only disposable loopback HTTP services, synthetic resources, deterministic validators, and fake identities. No real credential, production token, customer record, or private production response entered the shared cache. Every result below is labelled either as a protocol requirement, actual lab observation, implementation behavior, or engineering inference. No unexecuted test is presented as evidence, and no latency or throughput result is invented.
Why cache revalidation needs acceptance testing
A successful conditional request does not, by itself, prove cache correctness. The origin can correctly produce 304 Not Modified while a cache has selected the wrong stored variant. A cache can also obey an ETag correctly yet violate private, mishandle Vary, or accidentally reuse a personalized representation for another user. RFC 9111 therefore conditions reuse on more than freshness: the URI, applicable method, Vary dimensions, cache directives, and validation state all matter.
There is another important distinction: “not stale” must mean not unintentionally stale. HTTP permits serving stale content in defined circumstances, including explicit stale extensions or other authorization to do so. RFC 5861's stale-while-revalidate, for example, deliberately permits a stale response for a bounded interval while asynchronous validation occurs. A system requiring strict blocking revalidation must not silently enable that behavior on the protected path.
Acceptance therefore needs two independent safety invariants. A freshness invariant says the cache may reuse an entry only while fresh, after successful validation, or under an explicitly approved stale policy. An isolation invariant says the representation selected for one request must be eligible for that request's cache key, variant dimensions, authentication context, and shared/private caching rules. A correct validator cannot repair a violation of the second invariant.
Acceptance risk | Failure signature | Required evidence | Evidence class |
Fresh object unnecessarily revalidated | Origin sees conditional request during valid freshness window | Origin request log plus cache state | Implementation behavior |
Stale object reused without permission | Old bytes returned with no successful validation or approved stale directive | Age/freshness calculation, request trace | Protocol + observation |
Changed object hidden behind old validator | Origin changed representation but returned matching validation result | Body identity plus validator history | Origin correctness |
Wrong Vary variant reused | A request receives bytes generated for another variant | Request headers, Vary, representation ID | Protocol + observation |
Personalized response crosses users | Bob receives Alice's representation | Synthetic identities and cache storage trace | Security acceptance |
private or no-store ignored | Shared cache stores prohibited response | Cache inventory and repeat requests | Protocol + observation |
304 treated as new body | Empty 304 replaces stored representation | Client body and cache metadata trace | Protocol + observation |
Race returns old generation | Concurrent clients see inconsistent validator/body pair | Coordinated revalidation trace | Implementation behavior |
The acceptance target is not “the cache makes fewer origin requests.” It is that every cache-produced representation has a defensible protocol path from the current request to the stored response. Performance can be measured separately after correctness is established.
Define freshness, validation, and representation identity
RFC 9111 defines freshness mathematically: a stored response is fresh when its freshness lifetime is greater than its current age. Explicit freshness can come from s-maxage for a shared cache, then max-age, then Expires according to the specification's precedence rules. Once a response is not fresh, reuse generally requires validation or an explicit allowance for stale service.
Validation answers a different question. A conditional request asks the recipient whether a stored representation remains suitable relative to a validator. If-None-Match compares entity tags and is the preferred cache-validation mechanism when an ETag is available. If-Modified-Since is typically the fallback when a stored response has Last-Modified but no ETag. When both are supplied, If-None-Match takes precedence and If-Modified-Since is ignored.
For acceptance testing, “representation identity” needs a test-visible meaning. The lab added X-Representation-Id solely as instrumentation. It is not an HTTP standard header and is not part of normal cache semantics. It made it possible to record that body, validator, variant, and synthetic principal belonged to the expected representation without inferring identity from status code alone.
Concept | Acceptance meaning | What does not prove it |
Freshness | Stored response remains inside its permitted freshness lifetime | A recent-looking ETag |
Validation | Origin/cache successfully establishes that a stored response can be reused | Merely sending a conditional header |
ETag | Opaque validator for the selected representation | User identity or authorization |
Last-Modified | Modification-time validator usable for conditional retrieval | Byte-level uniqueness |
Representation identity | Exact synthetic body/variant/user generation expected by the fixture | URI alone |
Cache key | Request dimensions used to select eligible stored responses | ETag alone |
Shared cache | Cache whose stored response can potentially serve multiple users | Browser history |
Private cache | Cache dedicated to one user context | Cache-Control: private as an authorization system |
Strong versus weak validators and what they actually prove
RFC 9110 defines a strong validator as one that changes whenever representation data changes in a way observable in a successful GET representation. It also requires strong validators to remain unique across versions over time. Weak validators deliberately provide a looser equivalence relation: two byte-different representations can legitimately share a weak validator when the origin considers them equivalent for the validation purpose.
That distinction is particularly important for If-None-Match, because HTTP explicitly requires weak comparison for that precondition. A weak ETag can therefore validate a cached representation even when the current representation's bytes are not identical. Cache validation should consequently be tested for semantic validator behavior, not incorrectly treated as cryptographic proof that every byte is unchanged.
Build a synthetic HTTP server with deterministic validators
The executed fixture used two ThreadingHTTPServer instances bound only to loopback on ephemeral ports: one origin and one controlled shared-cache proxy. That boundary mirrors the kind of request/response work practiced in backend development, but the acceptance evidence here comes from the HTTP specifications and this isolated lab rather than from any production platform.
The origin generated deterministic strong ETags from a truncated SHA-256 digest of the synthetic response body. The test harness could mutate resource state between requests, advance a logical cache clock without sleeping, and record all incoming conditional headers. Fixed Last-Modified timestamps prevented wall-clock timing from becoming hidden evidence.
The proxy stored entries by URI plus the dimensions named by Vary. It recorded cache disposition using a local Cache-Status-style header. RFC 9211 standardizes such concepts as hit and forwarded cache states, but LocalTestCache was simply the fixture's own cache identifier.
Endpoint | Synthetic behavior | Cache policy | Identity evidence |
/etag | Strong ETag changes with body | max-age=2 | etag:<digest> |
/lm | No ETag; fixed Last-Modified | max-age=0 | lm:v1, lm:v2 |
/vary | Body selected by X-Rep | max-age=60, Vary: X-Rep | vary:A:*, vary:B:* |
/personal | Fake Alice/Bob/anonymous bodies | Authenticated: private; anonymous: public | personal:<principal> |
/nostore | Synthetic nonsecret body | no-store | request generation |
/nocache | Stable ETag | no-cache | validator + body |
/must | Can simulate origin failure | max-age=0, must-revalidate | fixed body ID |
/weak | Byte formatting can change under same weak tag | max-age=0 | weak semantic version |
A representative deterministic validator helper was:
def strong_etag(body: bytes) -> str:
digest = hashlib.sha256(body).hexdigest()[:12]
return f'"{digest}"'For /etag, alpha-v1 produced "1045d9a162b5" in the executed fixture, and changing the body to alpha-v2 produced "52d7699527d7". Those values are test artifacts, not recommended validator formats. HTTP treats entity tags as opaque; an application is free to choose an implementation that satisfies the validator requirements.
The fake authentication strings such as Bearer alice and Bearer bob were deliberately nonsecret markers understood only by the local fixture. They must never be replaced with real API tokens in a shared-cache test.
Validate ETag and If-None-Match behavior
If-None-Match is the central conditional request for ETag-based HTTP cache revalidation. RFC 9110 says it is primarily used by conditional GET to update cached information efficiently. When the selected representation matches one of the supplied tags under weak comparison, a GET or HEAD condition that evaluates false results in 304; otherwise the normal representation can be returned.
The executed sequence established four distinct states rather than treating “ETag works” as a single test. First, an empty cache requested /etag. Second, an immediate repeat stayed inside the two-second logical freshness window. Third, the clock advanced past freshness without changing the origin. Fourth, the origin representation changed before another stale revalidation.
Step | Cache state before request | Proxy → origin condition | Origin result | Client-visible result | Observed representation |
Initial GET | Empty | None | 200 | 200, stored | alpha-v1, ETag "1045d9a162b5" |
Fresh repeat | Fresh | No origin request | Not applicable | 200, cache hit | Same body/tag |
Stale, unchanged | Stale | If-None-Match: "1045d9a162b5" | 304 | 200 from stored body | alpha-v1, same tag |
Stale, changed | Stale | Old If-None-Match | 200 | 200, cache replaced | alpha-v2, ETag "52d7699527d7" |
Actual observation: the four client requests caused three origin requests. The fresh second request did not contact the origin. The third request produced LocalTestCache; fwd=stale; fwd-status=304, while the changed fourth request produced a full 200 and replaced the stored entry.
That result demonstrates three separate invariants in the local implementation. Fresh content was reusable without validation. A stale unchanged representation was reused only after the origin validated its ETag. A stale changed representation was not masked by the old validator; the full new response replaced it.
It is important not to confuse this with mutation concurrency. Refonte Learning's If-Match lost-update article addresses the complementary problem of protecting writes against stale versions. That article itself separates If-Match write concurrency from If-None-Match read caching; this playbook stays on the cache-validation side.
Validate Last-Modified and If-Modified-Since behavior
If-Modified-Since makes GET or HEAD conditional on whether the selected representation has been modified after the supplied date. RFC 9110 describes cache updating as one of its normal uses, particularly when an ETag is unavailable. When If-None-Match is present, however, the recipient must ignore If-Modified-Since because the ETag condition is considered the more accurate replacement.
The /lm endpoint deliberately omitted ETag so that the test could not accidentally succeed through If-None-Match. Its first version returned Last-Modified: Tue, 14 Nov 2023 22:13:20 GMT and Cache-Control: max-age=0, making validation necessary on subsequent reuse.
Step | Stored validator | Conditional request observed at origin | Origin response | Client result | Representation ID |
Initial | None | None | 200 + Last-Modified | 200 | lm:v1 |
Unchanged revalidation | 22:13:20 GMT | If-Modified-Since: Tue, 14 Nov 2023 22:13:20 GMT | 304 | Stored body reused | lm:v1 |
Changed origin | Old timestamp | Same old IMS value | 200, new timestamp 22:13:30 | New body | lm:v2 |
Actual observation: no If-None-Match appeared in the /lm origin trace. On the unchanged request, the proxy reused the previously stored body only after receiving 304. After the origin advanced Last-Modified by ten seconds and changed the body, the same old conditional date no longer validated; the origin returned the complete new representation.
The acceptance implication is deliberately narrow. This proves that the local cache correctly used Last-Modified fallback for this deterministic fixture. It does not prove that modification times offer the same identity precision as strong ETags. Timestamp resolution, server clock behavior, and application update patterns can make time-based validators less discriminating than a purpose-built entity tag. RFC 9110 consequently gives If-None-Match precedence when both are present.
Test 304 responses and representation reuse
A 304 is not a lightweight substitute representation. RFC 9110 defines it as the result of a conditional GET or HEAD that would otherwise have produced 200 had the condition not evaluated false. A 304 terminates after its headers and cannot contain response content or trailers.
This matters when testing through an intermediary. The origin may produce 304, while the cache produces 200 to the downstream client by combining the validation result with its already stored representation. RFC 9111 explicitly permits the stored response to be updated and reused following successful 304 validation. A test that records only the browser/client status can therefore miss the validation transaction entirely.
Observation point | Correct unchanged-resource behavior | What would fail acceptance |
Cache → origin | Conditional GET with stored validator | Unconditional stale reuse where validation is required |
Origin → cache | 304 with applicable metadata | 304 carrying a new response body |
Cache store | Existing body retained; metadata updated as applicable | Stored body replaced with empty 304 payload |
Cache → client | May be reconstructed 200 with cached representation | Wrong representation or variant |
Identity check | Body ID remains same validated generation | Validator points to one generation while body is another |
The 304 response is a validation result, not a new representation
The /etag and /lm runs both observed this distinction. The origin's 304 response did not contain the resource body. The proxy retained its existing stored bytes, processed the validation result, and returned a complete 200 representation to the test client.
RFC 9110 also requires specified metadata to be generated on a 304 when that metadata would have appeared on the corresponding 200, including fields relevant to cache updating such as ETag and Vary where applicable. The acceptance harness should therefore preserve origin-response headers separately from downstream-response headers rather than collapsing them into one log record.
A robust evidence record is thus a chain: request identity → selected stored entry → outgoing conditional header → origin validation status → metadata update → reused body identity. “Received 304” proves only the middle of that chain.
Validate Cache-Control directives and freshness boundaries
Cache-Control determines whether a stored response can be reused and under what conditions. max-age establishes a freshness lifetime; s-maxage takes precedence for shared caches when present. RFC 9111's freshness test is strict: a response is fresh when freshness_lifetime > current_age, not when the values are equal.
Directive vocabulary also needs precise acceptance semantics. no-cache does not mean “do not store.” It requires successful validation before reuse. no-store prohibits storing any part of the applicable request/response for later use. private prevents storage by a shared cache while still allowing a private cache under the specification's rules. must-revalidate prevents stale reuse after expiry until validation succeeds.
Directive tested | Protocol expectation | Actual local observation | Acceptance |
max-age=2 | Reuse while fresh; validate when stale unless other permission applies | Immediate repeat hit cache; post-expiry request revalidated | Pass |
no-cache | May be stored, but cannot satisfy later request without validation | First response stored; second use sent conditional request and received 304 | Pass |
no-store | Must not be stored or reused | Two calls both reached origin; no cache entry created | Pass |
private | Shared cache must not store unqualified private response | Alice/Bob responses never entered shared store | Pass |
must-revalidate | Once stale, do not reuse until successful validation | Simulated origin 503 was forwarded instead of old body | Pass |
s-maxage | Shared-cache freshness overrides max-age/Expires; stale reuse requires validation | Protocol gate documented; not claimed as an executed fixture case | Not used as lab evidence |
Cache-Control versus application authorization
Cache directives are not an authorization system. private can stop a conforming shared cache from storing a response, but it does not determine whether Alice is allowed to access /account/42. Likewise, ETag possession does not grant permission. Authentication and object-level authorization remain application decisions before the application exposes personalized content.
RFC 9111 adds an important shared-cache rule for requests containing Authorization: a shared cache must not reuse a cached response to such a request unless the response has a directive that explicitly permits shared-cache storage under the applicable requirements. The specification identifies directives including must-revalidate, public, and s-maxage as having that effect.
That means “an Authorization header automatically makes every response private forever” is also too simplistic. An origin can intentionally authorize shared caching through the protocol. Acceptance must test the application's actual policy, not rely on folklore. For personalized account data in this playbook, the policy is deliberately conservative: authenticated responses are private and never enter the shared store.
Test Vary and representation-specific cache keys
The cache key cannot safely be reduced to URL alone. RFC 9110 says Vary identifies request fields, beyond method and target URI, that may have influenced representation selection. RFC 9111 then prohibits reuse of the stored response without revalidation unless the request fields nominated by Vary match the request that produced the stored response.
The lab used X-Rep instead of a production content-negotiation field because the goal was deterministic evidence. /vary returned Vary: X-Rep; requests carrying X-Rep: A generated one synthetic body and validator, while X-Rep: B generated another.
Request | Expected variant | Cache state | Origin contacted? | Observed body/ID |
First X-Rep: A | A | Miss | Yes | representation-A, vary:A:"5f7395e05e67" |
First X-Rep: B | B | Miss | Yes | representation-B, vary:B:"cbee62ee585c" |
Repeat A | A | Matching variant hit | No | A again |
Repeat B | B | Matching variant hit | No | B again |
Vary as a cache-key dimension
Actual observation: four client requests generated only two origin retrievals because each variant was stored independently. The repeat for A never received B's body, ETag, or representation identifier; the repeat for B likewise remained isolated.
Internally the fixture recorded variant keys equivalent to (('x-rep', 'A'),) and (('x-rep', 'B'),). That internal structure is implementation-specific. What HTTP requires is the matching behavior, not that exact data structure. RFC 9111 even permits normalization of nominated request fields in appropriate cases before comparison.
The production acceptance lesson is broader than X-Rep. If representation selection depends on Accept-Encoding, Accept-Language, or another request field, that dimension must be reflected in the response's Vary semantics when required. A perfectly valid ETag attached to the wrong selected variant is still the wrong cache answer.
Vary should also not be misused as a substitute for authorization isolation. RFC 9110 specifically treats authorization-based reuse through separate cache rules rather than requiring Authorization to become an ordinary Vary dimension. Authorization safety belongs in authenticated-response cacheability policy.
Test personalized responses and shared-cache isolation
The highest-severity acceptance failure is not a slightly old public asset. It is a personalized representation crossing users. The fixture therefore exercised fake authenticated identities through the same local shared proxy.
/personal returned private, max-age=60 for synthetic authenticated principals and a publicly cacheable representation for the anonymous case. RFC 9111 requires a shared cache not to store a response carrying an unqualified private directive.
Request | Origin body | Cache directive | Stored by shared cache? | Repeat behavior |
Fake Alice | private-profile:alice | private, max-age=60 | No | Alice repeat reached origin |
Fake Bob | private-profile:bob | private, max-age=60 | No | Independent origin response |
Fake Alice again | private-profile:alice | private, max-age=60 | No | No Bob/Alice shared hit |
Anonymous first | anonymous representation | public cacheable policy | Yes | Entry stored |
Anonymous repeat | anonymous representation | same | Yes | Shared-cache hit |
/nostore twice | synthetic nonsecret content | no-store | No | Both calls reached origin |
Personalized responses in shared-cache scenarios
Actual observation: Alice's first response was not stored, Bob's response was not stored, and Alice's second request went back to the origin rather than reusing either authenticated response. The anonymous representation was stored and subsequently hit. Two /nostore requests also caused two origin requests with no persistent entry.
This supports a narrowly scoped conclusion: the controlled shared-cache implementation honored the tested isolation directives for these synthetic identities. It does not establish the same behavior for an untested CDN, reverse proxy, service worker, browser cache, or application gateway.
The security acceptance rule should therefore inspect both positive and negative evidence. It is not enough to show Bob did not receive Alice's body once. The cache inventory should show that Alice's representation was never eligible for shared storage in the first place. That reduces dependence on accidental key differences or test ordering.
Authorization checks also remain necessary when a request misses cache. Correct cache isolation cannot rescue an origin that improperly authorizes Bob to fetch Alice's object. Cache correctness and authorization correctness are separate controls whose combined result prevents unauthorized data exposure.
Exercise validator changes, concurrent requests, and revalidation races
Revalidation creates a concurrency boundary. When many requests arrive after one popular entry becomes stale, an implementation can forward every request independently or collapse requests so one revalidation updates the entry and waiting requests reuse the result. RFC 9111 explicitly permits request collapsing, but it does not require every cache to implement the same synchronization strategy.
The local proxy used a per-key lock. The fixture first stored alpha-v1, changed the origin to alpha-v2-race, advanced the logical clock beyond the freshness lifetime, and then released eight client threads against the same stale key.
Race evidence | Actual observation |
Concurrent client requests | 8 |
Cached generation at race start | alpha-v1 |
Origin generation before release | alpha-v2-race |
New strong ETag | "a1ecc8e6e0b2" |
Additional origin requests during race | 1 |
First cache disposition | stale forward, origin 200, stored |
Remaining dispositions | 7 cache hits after update |
Client bodies | 8 × alpha-v2-race |
Client ETags | 8 × "a1ecc8e6e0b2" |
Revalidation race conditions
Actual observation: one thread acquired the revalidation path, received the new 200 representation, and updated the cache. The seven other requests then used that updated entry. All eight responses paired the new body with the new ETag. No client received the old alpha-v1 generation.
The fact that only one additional origin request occurred is implementation behavior, not a general HTTP guarantee. Another conforming cache could send multiple concurrent conditional requests and still be correct if every response is handled safely. This test therefore establishes local synchronization correctness, not an industry-wide request-collapsing property.
The acceptance invariant is about representation generation, not optimization: a losing revalidation must not overwrite a newer accepted representation, and concurrent requests must not combine the body from one generation with the validator or metadata of another. Implementations need enough synchronization or version checking to uphold that invariant.
A separate decision is whether stale content may be served while revalidation proceeds. stale-while-revalidate explicitly permits that for its configured interval. A requirement that “no stale representation leaves this boundary during revalidation” is incompatible with enabling such a stale window on that path unless the acceptance definition explicitly allows it.
Test unsafe assumptions around weak versus strong validators
Weak and strong ETags are not interchangeable evidence. If-None-Match intentionally uses weak comparison, so W/"semantic-1" can validate a stored response even when the current representation's bytes differ. That is protocol-defined behavior, not necessarily a stale-cache bug.
The /weak fixture made this visible. Its first body was weak semantic value with ETag W/"semantic-1". The origin then changed only formatting by adding additional spaces while deliberately retaining the same weak tag. On revalidation, the origin judged the representations equivalent under its weak-validator policy and returned 304.
Origin transition | Validator | Conditional result | Cache-visible bytes | Interpretation |
Initial semantic version | W/"semantic-1" | Initial 200 | Original spacing | Stored |
Byte-only formatting change | Still W/"semantic-1" | 304 | Old cached bytes reused | Valid weak equivalence |
Semantic version changes | W/"semantic-2" | Full 200 | New body stored | Previous weak equivalence ended |
Actual observation: after the formatting-only origin change, the cache continued to return its previously stored bytes because the unchanged weak tag validated them. When the origin advanced the tag to W/"semantic-2" for a semantic change, the cache received a full 200 and replaced the representation.
The unsafe assertion would be: “304 proves that the origin currently stores exactly the same bytes as the cache.” That statement is false when weak validators are involved. A strong validator is appropriate when byte-observable representation changes must invalidate the prior version; even then, the validator is opaque to the client, so acceptance should compare behavior rather than reverse-engineer tag contents.
This is another reason not to drift into If-Match semantics. Strong comparison has important uses for state-changing preconditions, but the cache-validation rule tested here is the weak comparison mandated for If-None-Match. Treating cache revalidation as if it were optimistic write concurrency would test the wrong protocol contract.
Validate browser/proxy observations against protocol semantics
A local proxy gives excellent observability because every lookup, forwarded request, conditional header, and storage decision can be recorded. It is not evidence that every browser uses the same key structure or exposes the same state.
RFC 9111 defines the HTTP cache requirements, while the WHATWG Fetch Standard specifies browser-oriented fetch processing and HTTP-cache integration. Browser implementations can also partition cache state in ways that are not represented by this fixture's simple URI-plus-Vary key. Product behavior should therefore be validated at the layer being accepted rather than inferred from a custom proxy.
That distinction is especially useful for engineers crossing frontend development and cloud development: the request may travel through browser cache logic, service infrastructure, gateways, and shared intermediaries, but each layer needs its own evidence.
Question | Protocol-defined? | Local lab observed? | Safe conclusion |
Can a fresh stored response be reused? | Yes, subject to reuse conditions | Yes | Local implementation conforms for tested case |
Does If-None-Match use weak comparison? | Yes | Yes through weak fixture | Protocol rule confirmed by fixture |
Must Vary dimensions match for ordinary reuse? | Yes | Yes | Local variant isolation passed |
Does private bar shared storage? | Yes | Yes | Local shared cache passed |
Will every browser use identical internal key structure? | No | Not tested | Do not claim |
Will every CDN collapse concurrent revalidations? | No | Not tested | Do not claim |
Does a browser necessarily expose local Cache-Status detail? | No general guarantee from this fixture | Not tested | Do not claim |
Does one local pass establish production edge correctness? | No | No | Production implementation requires its own run |
Cache-Status is particularly useful as an observability vocabulary, but even that must be interpreted correctly. RFC 9211 provides standardized fields for reporting cache handling; its existence does not mean every intermediary emits the header, nor that an intermediary's absence of the header proves there was no cache.
A production acceptance run should therefore collect evidence at every controllable boundary: client request, shared-cache decision, forwarded origin request, origin response, and downstream representation. Where a proprietary cache supplies documented debug headers or logs, those can replace the fixture's synthetic LocalTestCache marker. Their semantics should be tied to that implementation's documentation.
Build an evidence matrix and acceptance gates
Acceptance should finish with evidence that another reviewer can audit without reconstructing intent from console output. For every case, preserve the request headers relevant to cache selection, the origin response status and validators, cache directives, Vary, cache disposition, and final representation identity.
The protocol evidence also needs to be distinguished from the lab evidence. RFC 9111 says a cache must not reuse a response unless its target URI, method, Vary selection, validation requirements, and freshness/stale rules allow that reuse. The lab trace then answers whether this particular implementation actually met those conditions.
Acceptance case | Required protocol outcome | Executed evidence | Result |
First ETag request | Miss may retrieve/store eligible 200 | Origin 200, "1045d9a162b5", stored | Pass |
Fresh ETag request | Fresh eligible response reusable | Cache hit; origin not called | Pass |
Stale unchanged ETag | Conditional validation before reuse | If-None-Match, origin 304, stored body reused | Pass |
Changed ETag | Old tag must not validate new representation | Origin 200, "52d7699527d7", new body | Pass |
Last-Modified fallback | IMS can validate when no ETag | IMS observed; unchanged 304 | Pass |
Changed Last-Modified | Changed representation returned fully | 200 with new time and lm:v2 | Pass |
304 processing | Reuse existing representation, not 304 body | Client received stored complete body | Pass |
no-cache | Validate before every reuse | Second request conditionally validated | Pass |
no-store | Do not store/reuse | Two origin calls; no entry | Pass |
private personalization | Shared cache must not store | Alice/Bob never stored | Pass |
Vary: X-Rep | A and B selected separately | Two variants, independent hits | Pass |
Concurrent stale entry | No old/new generation corruption | Eight clients received new generation | Pass |
must-revalidate + failure | Stale copy not reused | Origin 503 forwarded | Pass |
Weak validator | Byte difference may remain equivalent | 304 reused old bytes under same weak tag | Pass |
The release gate should be stricter than “all expected statuses appeared.” A passing trace must preserve the tuple request selection → eligible cache entry → validator → response metadata → representation identity. Any unexplained mismatch in that chain is a failure even when the final HTTP status looks conventional.
Gate | Pass condition | Block release when |
Validator integrity | Each strong tag maps consistently to the tested representation generation | Same strong tag appears on observably changed tested representation |
Freshness | No stale reuse outside explicitly allowed policy | Stale body served without successful validation or documented permission |
304 handling | Stored body retained and appropriate metadata refreshed | 304 treated as independent representation |
Variant isolation | Every Vary dimension participates in matching | Variant A can answer variant B |
Shared/private boundary | Private and no-store content absent from shared inventory | Personalized entry becomes shared-cache candidate |
Auth boundary | No fake principal receives another principal's body | Any cross-principal representation observed |
Failure behavior | must-revalidate blocks stale fallback | Origin failure causes prohibited stale reuse |
Concurrency | All returned body/validator pairs belong to one valid generation | Mixed generations or stale overwrite |
Scope discipline | Claims limited to implementation actually tested | Local run generalized to browsers/CDNs without evidence |
There is one subtle gate around permitted stale behavior. RFC 9111 prohibits generating stale responses when an applicable directive forbids them, and otherwise requires a basis such as disconnection, an explicit request/origin permission, an extension such as RFC 5861, or an appropriate out-of-band contract. Acceptance documentation should therefore list every intentional stale pathway rather than treating “stale” as universally forbidden.
For this fixture, no stale-while-revalidate window was used in the strict paths. The concurrent stale test blocked behind a revalidation/update instead of intentionally returning the prior generation. That is a local design decision consistent with the acceptance objective, not a requirement that every HTTP cache adopt the same concurrency strategy.
Final acceptance decision and Refonte Learning CTA
Decision item | Final status |
Local ETag revalidation | Accepted |
Local Last-Modified fallback | Accepted |
304 stored-representation reuse | Accepted |
Tested freshness/directive handling | Accepted |
Vary variant isolation | Accepted |
Synthetic authenticated-user isolation | Accepted |
Concurrent local revalidation | Accepted |
Weak-validator semantics | Accepted with documented byte-equivalence limitation |
Browser/CDN equivalence | Not claimed; requires implementation-specific evidence |
Production-data safety | Lab only; no real credentials or customer data permitted |
3 months; 10–12 hours/week; REST; GraphQL; authentication and authorization; database integration; API documentation and testing; error handling and logging; versioning and deprecation; microservices architecture; performance optimization; API security best practices |
Build broader API engineering skills through Refonte Learning’s APIs Developer Fundamentals.
Use its three-month, 10–12-hours/week program without assuming HTTP caching internals are specifically taught.
