A framework's in-process response cache is keyed by request path, and users start seeing each other's personalized pages - why?
answer
- the key is the whole contract
- the component cannot see what the handler read
- path alone claims one body for everyone
- placement decides whether checks are skipped
- identity in the key destroys the hit rate
basics
~20 sThe key omits everything the body varies on. Keyed by path alone, the first caller's rendered page is stored and replayed to everyone asking for that path. Fix the key, or refuse to cache identity-dependent responses.
solid answer
~40 sAn in-process response cache stores a rendered response under a key you choose and replays it on a later request with the same key. The component has no idea what the handler read - it sees a request and a finished body - so if the key is just the path, two requests that produce different bodies collide and the second caller gets the first caller's page. Worse, a cache consulted early in the chain can answer **before** authentication and authorization run, turning a privacy leak into a bypass. Two fixes exist and are not equivalent: compose the key from every input the body varies on, or - safer - make the cache opt-in and admit only responses a route declares identity-free. Personalized pages belong in the second bucket.
code
pseudocode · 11 lineskey = method + ":" + path + ":" + normalizedQuery(request)
+ ":" + negotiatedFormat + ":" + negotiatedLanguage
if route.declaresIdentityFree and not request.hasResolvedIdentity:
entry = cache.get(key)
if entry: return entry
response = next(request)
if response.status == 200: cache.put(key, response, shortTtl)
return response
return next(request)go deeper
Take away the core idea: a cache hands back whatever was stored under the key it was given, so two requests with the same key get the same body even when they should not.
Be able to list what belongs in the key - identity, negotiated format and language, normalized query, flags - and explain why the component cannot infer any of it by itself.
Separate the two harms and name the evidence: a leak from a bad key versus a check bypass from an early lookup, confirmed by cold-starting an instance and replaying two identities against the same path.
Argue the policy: deny by default, explicit per-route eligibility, a review rule that revisits eligibility whenever a handler starts reading identity, and a clear-eyed view of whether the cache earns its risk at all.
## What the component knows and does not know An in-process response cache is a piece of the chain that sits ahead of the handler, looks for a stored entry under a key, and either returns it or lets the request continue and stores what comes back. Everything it does is driven by that key. It observes the request and the finished response; it does **not** observe what the handler consulted to build the response - the resolved identity, the session contents, a permission check, a feature flag, a locale decision made three layers down. None of that is visible to it unless the key says so. So the rule is blunt: **whatever the body varies on must be in the key, or the cache will serve the wrong body.** A key of path alone asserts that this path produces one body for everybody, forever. For a personalized page that assertion is false, and the consequence is exactly the reported symptom - the first request through after a cold start populates the entry, and everyone else gets that person's page until it expires. ## Why it is worse than a wrong answer There are two distinct harms, and a good answer separates them. 1. **Disclosure.** One caller's data is rendered into another caller's browser. Depending on the page, that can be personal data, account state, or anything the first caller could see. 2. **Check bypass.** If the cache is consulted before the stage that authenticates and authorizes the request, a hit returns without those stages running at all. An anonymous or under-privileged caller then receives a page that the checks would have refused. The entry does not have to be personalized for this to matter - any protected page stored under an unauthenticated-looking key is now reachable without the check. The second harm is decided by **placement in the chain**, not by the key, which is why both have to be reasoned about. A cache placed after the identity is resolved can still leak through a bad key; a cache placed before it leaks through placement no matter how good the key is. ## Composing a key that is actually true If a response really must be cached and really does vary, the key has to name every varying input: - method and path, plus the **query parameters that change the body** (normalized - ordering and unknown parameters otherwise fragment or collide the space); - the **caller identity or session**, where the body depends on it; - the **negotiated format and language**, where content negotiation happens; - any **flag or experiment assignment** that changes what is rendered. Two dangers ride along. A key that includes identity gives every user a private copy, which multiplies the memory the cache holds and usually collapses the hit rate to near zero - the caching was probably not worth doing. And a key derived from a header the caller controls lets a caller choose which bucket to write into, so untrusted inputs need normalizing to a small known set before they touch the key. ## The safer default: opt in, not opt out Because the failure is silent and its blast radius is other people's data, the defensible arrangement is deny-by-default: the cache admits nothing unless the route declares it eligible, and eligibility means the response is a pure function of inputs already in the key and does not depend on who is asking. Practical guards: | Guard | What it prevents | |---|---| | Route must opt in explicitly | silent caching of a page nobody reviewed | | Refuse entries when an identity was resolved | storing a personalized body at all | | Refuse when the response declares restricted reuse | contradicting the route's own statement | | Place the lookup after the checks | answering before authorization runs | | Include negotiation and normalized query in the key | serving the wrong format or language | ## Confirming the diagnosis The pattern is distinctive and easy to prove. The wrong page always belongs to a *real* other user, not to nobody. It appears for a bounded period and then rights itself - the entry's lifetime. Restarting or redeploying an instance clears it, and it comes back as soon as the first personalized request repopulates the entry. And when several instances run, different callers see different wrong pages at the same time, because each process holds its own copy. Reproduce it directly: cold-start one instance, request the page as one user, then request the same path as another and compare. If the second response carries the first user's content, the key is the bug.
- Putting the caller's identity into the key removes the leak. Why is that often still the wrong fix?Because it gives each user a private copy: memory grows with the user count and the hit rate collapses to repeats by the same person, which is rarely worth the entry. If the page is personalized, the honest conclusion is usually that a shared response cache is the wrong layer for it.
- Why does placing the cache lookup early in the chain matter beyond the key?A hit returns without running the stages behind it. If authentication and authorization are among those stages, a stored protected page becomes reachable without any check at all - a bypass rather than a leak. Placing the lookup after the checks confines the failure mode to the key.
- What makes this bug so hard to spot before production?It needs two different callers hitting the same instance within one entry's lifetime, which a single-user test and most functional suites never produce. It also self-heals on expiry and restart, so reports arrive as unreproducible one-offs unless you cold-start and replay two identities deliberately.
saying these in an interview costs you the question
- Assumes the cache can tell that a response was personalized
- Thinks the path uniquely identifies a response body
- Treats the leak as a display bug rather than a disclosure
- Ignores that an early lookup can skip the authorization stage
- Adds identity to the key without noticing the hit rate collapse
- Builds the key from an unnormalized caller-controlled header