Skip to content

blackbull.middleware.cache

blackbull.middleware.cache

Response caching middleware (RFC 9111 — HTTP Caching).

Caches successful GET/HEAD responses in a per-worker, in-memory LRU. Subsequent matching requests are served directly from the cache without running the handler. Supports:

  • TTL — server-side max_age (default 300 s), overridable by the response's Cache-Control: max-age=… directive (or, when present, s-maxage=… which takes precedence for shared caches).
  • ETag — auto-generated as W/"<sha256-prefix>" over the response body when the application does not supply one. The client's If-None-Match header is honoured: a match yields a 304 Not Modified with no body, regardless of the cached entry's TTL.
  • Cache-Control respect — responses carrying no-store, private, or no-cache are passed through and not stored. Requests carrying no-store skip the cache lookup too.
  • Authorization header — by default, requests with an Authorization header are NOT served from cache and their responses are NOT stored (RFC 9111 §3.5). Override with cache_authenticated=True.

Variant-aware: the response Vary header is honoured (RFC 9110 §12.5.5). When a stored response carries e.g. Vary: Accept-Encoding, the varied request-header values are folded into the cache key so a brotli variant is never replayed to an identity client. A response with Vary: * is passed through and not stored.

What it doesn't do (yet):

  • No server-side invalidation API. Restart the worker (or wait for TTL) to clear.
  • No cross-worker sharing. The cache is per-process — each worker has its own. Documented limitation.

The store is keyed by (method, path, query_string) → a per-URL bucket that holds the response's Vary field names alongside its variant entries (one per distinct set of (field, request-value) pairs named by Vary). Keeping the vary fields inside the bucket means they can never be evicted independently of the entries they key (which the earlier two-LRU design allowed — orphaning the entries).

Usage::

from blackbull.middleware import Cache

app.use(Cache(max_age=600))     # 10-minute TTL

@app.route(path='/feed')
async def feed(conn, receive, send):
    ...   # served from cache for 10 min after first hit

Cache

Per-worker in-memory response cache.