"""Weak ETag + Cache-Control on public GET responses (API 1.1). Pure ASGI middleware so it can sit *inside* GZip (the hash is computed on the uncompressed JSON body — gzip output embeds a timestamp and would never be stable). `If-None-Match` matching the body hash → `304 Not Modified` with an empty body. Admin, docs and non-200 responses are left untouched.""" from __future__ import annotations import hashlib from typing import Any from starlette.types import ASGIApp, Message, Receive, Scope, Send PUBLIC_PREFIX = "/api/v1/" SKIP_PREFIXES = ("/api/v1/admin", "/api/v1/docs", "/api/v1/openapi.json", "/api/v1/api-keys") CACHE_CONTROL = "public, max-age=60, stale-while-revalidate=300" MAX_BUFFER = 8 * 1024 * 1024 # bodies above this are passed through unhashed def weak_etag(body: bytes) -> str: return 'W/"' + hashlib.sha1(body).hexdigest() + '"' def _etags_match(header: str | None, etag: str) -> bool: if not header: return False if header.strip() == "*": return True wanted = {t.strip() for t in header.split(",")} strong = etag.removeprefix("W/") return etag in wanted or strong in wanted or any((t.removeprefix("W/")) == strong for t in wanted) class ETagMiddleware: def __init__(self, app: ASGIApp) -> None: self.app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http" or scope.get("method") != "GET": await self.app(scope, receive, send) return path: str = scope.get("path", "") if not path.startswith(PUBLIC_PREFIX) or path.startswith(SKIP_PREFIXES): await self.app(scope, receive, send) return headers = {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope.get("headers", [])} inm = headers.get("if-none-match") start: Message | None = None chunks: list[bytes] = [] passthrough = False size = 0 async def send_wrapper(message: Message) -> None: nonlocal start, passthrough, size if passthrough: await send(message) return if message["type"] == "http.response.start": start = message if message.get("status") != 200: passthrough = True await send(message) return if message["type"] == "http.response.body": body = message.get("body", b"") size += len(body) if size > MAX_BUFFER: passthrough = True assert start is not None await send(start) for c in chunks: await send({"type": "http.response.body", "body": c, "more_body": True}) await send(message) return chunks.append(body) if message.get("more_body"): return assert start is not None full = b"".join(chunks) etag = weak_etag(full) raw_headers: list[tuple[bytes, bytes]] = [(k, v) for k, v in start.get("headers", []) if k.lower() not in (b"etag", b"cache-control")] raw_headers.append((b"etag", etag.encode("latin-1"))) raw_headers.append((b"cache-control", CACHE_CONTROL.encode("latin-1"))) if _etags_match(inm, etag): keep = {b"etag", b"cache-control", b"vary", b"x-content-type-options", b"referrer-policy", b"access-control-allow-origin"} hdrs = [(k, v) for k, v in raw_headers if k.lower() in keep] await send({"type": "http.response.start", "status": 304, "headers": hdrs}) await send({"type": "http.response.body", "body": b"", "more_body": False}) return await send({"type": "http.response.start", "status": 200, "headers": raw_headers}) await send({"type": "http.response.body", "body": full, "more_body": False}) return await send(message) await self.app(scope, receive, send_wrapper) def etag_of(value: Any) -> str: """Helper for tests and callers holding an already-rendered body.""" return weak_etag(value if isinstance(value, bytes) else str(value).encode()) __all__ = ["CACHE_CONTROL", "ETagMiddleware", "etag_of", "weak_etag"]