Framework-Agnostic HTTP Caching
FastAPI users get @fastapi_cache with automatic HTTP wiring. But the core HTTP cache primitives live in inhouse.http_cache and work with any framework. Flask, Django, Starlette, even raw WSGI - same ETag, Cache-Control, and 304 logic.
Core HTTP primitives
These functions have zero framework dependencies:
make_weak_etag(value)- stableW/"<sha256>"from any JSON-serializable valueetag_matches(if_none_match, etag)- exact token match in a comma-separatedIf-None-Matchheadercache_control_header(remaining_ttl, *, visibility=)- buildsCache-Controlstringhttp_cache_headers(...)- builds a header dict from TTL/etag flagshttp_cache_outcome(...)- decides 200 vs 304, returnsHttpCacheOutcome
Flask example
A Flask endpoint that caches product data in-process and emits HTTP cache headers.
from flask import Flask, request, jsonify, make_response
from inhouse import MemoryStore, inhouse_cache, http_cache_outcome
store = MemoryStore(max_size=1024, default_ttl=120)
app = Flask(__name__)
@inhouse_cache(store=store, etag=True)
def load_product(product_id: int) -> dict:
...
@app.route("/api/products/<int:product_id>")
def get_product(product_id: int):
body = load_product(product_id)
cache_key = f"load_product:{product_id}" # simplified for illustration
stored_etag = store.get_etag(cache_key)
if_none_match = request.headers.get("If-None-Match")
outcome = http_cache_outcome(
body,
if_none_match=if_none_match,
remaining_ttl=store.remaining_ttl(cache_key),
stored_etag=stored_etag,
http_cache=True,
cache_visibility="public",
use_etag=True,
)
if outcome.status_code == 304:
return "", 304, dict(outcome.headers)
resp = make_response(jsonify(outcome.body), 200)
for key, value in dict(outcome.headers).items():
resp.headers[key] = value
return resp
The same http_cache_outcome that @fastapi_cache uses internally, wired to Flask's request and response objects. You get ETag/304 support and Cache-Control headers without FastAPI.
Django example
A Django view that uses the same primitives.
import json
from django.http import HttpResponse, HttpResponseNotModified
from django.views import View
from inhouse import MemoryStore, inhouse_cache, http_cache_outcome
store = MemoryStore(max_size=1024, default_ttl=120)
@inhouse_cache(store=store, etag=True)
def load_article(slug: str) -> dict:
...
class ArticleView(View):
def get(self, request, slug):
body = load_article(slug)
cache_key = f"load_article:{slug}"
stored_etag = store.get_etag(cache_key)
if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
outcome = http_cache_outcome(
body,
if_none_match=if_none_match,
remaining_ttl=store.remaining_ttl(cache_key),
stored_etag=stored_etag,
http_cache=True,
cache_visibility="public",
use_etag=True,
)
if outcome.status_code == 304:
return HttpResponseNotModified(headers=dict(outcome.headers))
response = HttpResponse(
json.dumps(outcome.body),
content_type="application/json",
status=200,
)
for key, value in dict(outcome.headers).items():
response[key] = value
return response
Same core, different framework. The HTTP semantics are identical - only the response object construction differs.
Raw ASGI example
For maximal control, wire it directly into an ASGI application.
async def cached_product_app(scope, receive, send):
if scope["type"] != "http":
return
product_id = extract_product_id(scope["path"])
body = await load_product(product_id)
cache_key = f"load_product:{product_id}"
if_none_match = extract_header(scope, "if-none-match")
outcome = http_cache_outcome(
body,
if_none_match=if_none_match,
remaining_ttl=store.remaining_ttl(cache_key),
stored_etag=store.get_etag(cache_key),
http_cache=True,
cache_visibility="public",
use_etag=True,
)
await send({
"type": "http.response.start",
"status": outcome.status_code,
"headers": [(k.encode(), v.encode()) for k, v in dict(outcome.headers).items()],
})
if outcome.body is not None:
await send({
"type": "http.response.body",
"body": json.dumps(outcome.body).encode(),
})
Framework-agnostic etag-only (no response objects)
If you only need ETag metadata without building HTTP responses, use @inhouse_cache(etag=True) directly.
@inhouse_cache(store=store, etag=True)
def load_config(key: str) -> str:
...
value = load_config("theme")
etag = store.get_etag(f"__main__.load_config:{hash_args}")
remaining = store.remaining_ttl(f"__main__.load_config:{hash_args}")
Combine these with your framework's native response helpers. The ETag is stored as a weak W/"<sha256>" string, compatible with HTTP conditional request handling.
Which approach to use
| Approach | When to use |
|---|---|
@fastapi_cache | You use FastAPI/Starlette. Full automatic HTTP wiring. |
@inhouse_cache(etag=True) + manual http_cache_outcome | Flask, Django, or any non-Starlette framework. One extra step. |
@inhouse_cache(etag=True) only | You only need in-process caching with ETag metadata. Handle HTTP yourself. |
Raw primitives (make_weak_etag, etag_matches, etc.) | Custom protocols, non-HTTP caching, or building your own cache layer. |
The core primitives are framework-agnostic by design. http_cache_outcome returns a data class with status code, headers, and body - you decide how to turn that into a response.