A valid signature still needs a trust decision
An Oprindo sample had a matching asset hash and a valid signature, alongside an untrusted credential result. The useful story is in those separate answers.
A two-account preference example separates storage, validation and current access, including the case where an old ETag must not produce 304.
B asks for preferences and gets A’s dense layout. The application cache found /preferences successfully; that was the problem. Its path-only key treated two account-specific answers as equivalent. Adding the account would separate those bodies, but another question remains when A comes back after losing access. A correctly identified old answer is not automatically one the application may still give to its original reader.
The same shortcut is less alarming for a genuinely public opening-hours page. Two anonymous visitors can reasonably receive the same content for an agreed freshness interval. It is much less suitable for an invoice list, even if the list changes infrequently. Start by distinguishing the representations rather than applying one performance setting across every successful response. A cache that is correct for public information can be wrong for another route without its storage mechanism changing at all.
The downloadable programme uses synthetic accounts and validators to compare an origin decision with a faulty application dictionary. Its local checks call no browser, CDN or HTTP server. When the dictionary returns the wrong body, it avoids the very origin selection that would have separated the accounts. That is the path to follow in a deployed stack: which component answered, which inputs selected its representation and whether current access was checked before reuse. The stored value may be intact while the decision to offer it is wrong.
HTTP caching has directives for different questions. Under RFC 9111, unqualified private prevents shared-cache storage, while unqualified no-cache requires successful validation before reuse. no-store instructs caches not to store the request/response under its rules; it is not simply another spelling of always validate.[1] MDN's Cache-Control documentation is also useful when checking the practical distinction.[2] Read the exact directives rather than infer their behaviour from everyday meanings of private or cache.
The example proposes public, max-age=300 for /hours, private, no-cache for /preferences and private, no-store for /invoices. These are illustrative choices, not a universal production configuration. Five-minute freshness for hours is a product decision that tolerates a bounded delay in changes. Preferences may be stored privately but must be validated before reuse under the chosen policy. The invoice response is deliberately not offered for storage. A service with different data and obligations may make different choices, but should name the reason.
In the preference model, A first receives a body with account A and layout dense, plus ETag a-v1. A later request with its matching validator gets 304 and no replacement body. Under the HTTP conditional-response model, the cache can then reuse its stored representation.[3] B presenting A's validator gets 200 with B's body and b-v1 instead. The origin determines the representation for the current authorised account before comparing its validator. Matching a string supplied by the client cannot bypass that selection.
Revoke A’s access and present a-v1 again. The model returns 403 with no-store, not 304. Current access must be decided before the origin endorses reuse of an old body. This does not test browser logout, but it gives that later test a concrete expected distinction. Account-specific keys separate A from B; they cannot tell the origin whether A still has permission on this request. Both selection and current entitlement have to be right.
Proposed route policies in the HTTP-cache example.
Scroll sideways to see all columns.
| Route / directive | Storage and reuse meaning | Outside the claim |
|---|---|---|
/hours / public, max-age=300 | Shared freshness for the stated interval | Whether a closure requires faster invalidation. |
/preferences / private, no-cache | No shared storage; validate before reuse | Application dictionaries still need their own audience policy. |
/invoices / private, no-store | Do not store under the caching rules | Prior exports, screenshots and application state are separate. |
There may be several things called the cache in this journey: the browser, a shared intermediary and an application dictionary. HTTP rules do not automatically govern a dictionary that never reads them. In the faulty example, /preferences already points to A’s body when B asks, so the origin is never consulted. Adding private to the response cannot repair that earlier selection. Find which component chooses the body, because that is where the omitted audience condition has to be enforced.
An application key may need account, representation options and relevant policy context as well as path. Current access checks and invalidation still matter. Avoid declaring the problem solved merely because an identifier was added to the key. A user can remain in the same account while losing permission to a particular resource, and a stored value can survive beyond the event that made it unsuitable. Decide where those changes are checked and how existing entries are retired or prevented from answering.
Other representation dimensions can matter without involving identity. Language, format or selected fields can produce different bodies at the same path. HTTP Vary describes matching additional request-header dimensions for reuse; it does not invent application authorisation or make an incorrectly labelled public response private.[1] An application cache needs its own corresponding dimensions. Inspect the actual inputs used to build the body, then decide which equivalences are safe. A key should describe those choices rather than collect arbitrary headers without understanding them.
Give validators a coherent representation too. If different account bodies share one ETag, a conditional request can endorse the wrong stored answer. The model’s account/version strings make the separation easy to follow. Production validators need suitable generation, must change with the selected representation and should avoid revealing sensitive content. They validate a body; they are not session credentials. Keeping that difference visible prevents a matching client-supplied string from becoming a substitute for the access decision.
Executed synthetic origin/application-cache decisions. No browser, CDN or HTTP implementation was tested.
Scroll sideways to see all columns.
| Request | Validator / policy | Model response | Meaning |
|---|---|---|---|
A /preferences | No validator;private,no-cache | 200,A dense,a-v1 | Store privately;validate before reuse |
A /preferences | If-None-Match:a-v1 | 304,no body | Reuse only after current origin check |
B /preferences | A validator a-v1 | 200,B comfortable,b-v1 | A body is not B representation |
| A after access revoked | Old a-v1 | 403,no-store | Do not endorse old representation |
| B via bad app dictionary | Key=/preferences only | A body returned | Application cache audience defect |
Public /hours | public,max-age=300 | Proposed policy | Shared freshness is a product choice |
no-store. A no-store directive does not erase all previous copies or replace application-state, logging and access controls.For /hours, max-age=300 permits a period in which a cached answer can be fresh under the declared policy despite a later change at the origin. That can be a sensible trade-off for stable public information. It becomes a different promise if hours change during an exceptional closure and visitors need the latest notice immediately. The system needs an invalidation or update procedure consistent with that product need. Reducing every TTL to zero avoids one class of delay while giving up much of the performance benefit; it does not answer the audience question.
Private revalidation has another cost profile. The stored body may save transfer, but an origin decision is still required before reuse under no-cache. If origin availability is essential to the response, retaining a private copy does not automatically provide an offline fallback. Test the failure behaviour the service actually offers. A configuration allowing stale responses under some policy needs deliberate evaluation against that route's meaning, rather than a general assumption that any old answer is better than an unavailable one.
no-store gives an instruction within HTTP caching; it does not retract old exports, prevent screenshots or encrypt the body.[1] For invoices, therefore, inspect the application’s retained state and logs as well as compliant caches. Those copies need their own retention decisions. This is not a reason to abandon the header. It is a reason to use it for the mechanism it controls while accounting for the other places the information can go.
Trace the actual request with two authorised test identities and a controlled permission change. Inspect bodies, response headers, cache-status observations and whether an origin call occurs. A matching validator under A should meet a different decision from A’s validator presented by B, and revoked A needs the access decision again. Check the components that can answer before that decision: a CDN, cookie-dependent middleware or application dictionary may each take another path. The expected body and current permission matter together; a favourable status code alone cannot show that both were selected correctly.
There are routes for which the simplest correct policy is no caching, particularly while audience or invalidation requirements remain unclear. There are also public representations where shared caching is an excellent fit. The useful design is a per-representation decision that explains storage, audience and reuse conditions. It need not become a framework for every endpoint, but it should be clear enough that a later developer can tell why /hours and /invoices received different treatment.
Test both mistakes in the actual stack: B must not receive A’s body, and revoked A must not obtain reuse merely by presenting the old validator. Then describe whose representation can be stored, when it can answer again and which component checks those conditions. Faster responses become useful evidence after that selection is correct. Otherwise the performance gain may amount to returning the wrong answer with impressive efficiency.
Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.
See the Payment Operations PilotPrimary sources behind this article. Living documentation may change.

Practical engineering. A human perspective.
More about Hamsa Software and the person behind it. Editorial notes.