Problem
@cache stores a response in the shared backend even when the handler has marked it as not shareable, and then overwrites that marking:
@app.get("/me")
@cache(ttl=60)
async def me(request: Request, response: Response):
response.headers["Cache-Control"] = "private, no-store"
return {"user": request.headers.get("authorization")}
GET /me Authorization: Bearer alice -> {"user": "Bearer alice"}
GET /me Authorization: Bearer bob -> {"user": "Bearer alice"} Cache-Control: max-age=60
_cacheable_headers() (cache.py) drops cache-control, the entry is written anyway, and _with_cache_control() replaces the handler's header with the decorator's.
The same happens for two other signals that a response belongs to one caller:
- The request carries
Authorization. RFC 9111 §3.5 forbids a shared cache from reusing such a response unless it is explicitly public, s-maxage or must-revalidate.
- The response sets a cookie (
Set-Cookie is stripped from the entry, but the body it came with is stored and replayed to everyone).
The docs warn about authenticated endpoints (HTTP_CACHING.md, "Authenticated endpoints"), but nothing at runtime catches the mistake, and the result is one user's data served to another.
Proposal
On a miss, do not write the entry (and serve the response as the handler built it) when:
- the handler's response has
Cache-Control containing private or no-store; keep the handler's header instead of overwriting it;
- the response has
Set-Cookie;
- the request has
Authorization, unless the route is public=True (matching RFC 9111 §3.5) or a new opt-in such as cache_authorized=True is set (for per-user key builders).
Log each skip at DEBUG. Routes that do none of these are unaffected.
Related: #233 (header replay), #77 / #268 (Vary-aware keys).
Problem
@cachestores a response in the shared backend even when the handler has marked it as not shareable, and then overwrites that marking:_cacheable_headers()(cache.py) dropscache-control, the entry is written anyway, and_with_cache_control()replaces the handler's header with the decorator's.The same happens for two other signals that a response belongs to one caller:
Authorization. RFC 9111 §3.5 forbids a shared cache from reusing such a response unless it is explicitlypublic,s-maxageormust-revalidate.Set-Cookieis stripped from the entry, but the body it came with is stored and replayed to everyone).The docs warn about authenticated endpoints (HTTP_CACHING.md, "Authenticated endpoints"), but nothing at runtime catches the mistake, and the result is one user's data served to another.
Proposal
On a miss, do not write the entry (and serve the response as the handler built it) when:
Cache-Controlcontainingprivateorno-store; keep the handler's header instead of overwriting it;Set-Cookie;Authorization, unless the route ispublic=True(matching RFC 9111 §3.5) or a new opt-in such ascache_authorized=Trueis set (for per-user key builders).Log each skip at DEBUG. Routes that do none of these are unaffected.
Related: #233 (header replay), #77 / #268 (Vary-aware keys).