"""Static asset Cache-Control middleware. Injects ``Cache-Control`` headers on ``/static/`` and ``/releases/`` responses: - ``/static/*.css``, ``/static/*.js``, ``/static/*.map`` — far-future 1-year immutable (paired with ``?v=`` query strings in ``base.html``). - Other ``/static/`` paths — 1-day TTL (un-versioned assets like favicons). - ``/releases/*.tar.gz`` — 1-hour TTL for sdist tarballs; short enough for republishing, long enough to reduce origin load during install waves. - Any non-2xx response on ``/releases/`` — ``no-store`` so Cloudflare never caches 404s (which would break installs if a tarball is added after first access). """ from starlette.types import ASGIApp, Message, Receive, Scope, Send _FAR_FUTURE = "public, max-age=31536000, immutable" _ONE_DAY = "public, max-age=86400" _ONE_HOUR = "public, max-age=3600" _NO_STORE = "no-store" # Extensions that always carry a ?v= query string in base.html. # All other /static/ paths get the shorter 1-day TTL. _VERSIONED_EXTS = frozenset([".css", ".js", ".map"]) class StaticCacheMiddleware: """Pure ASGI middleware — zero overhead for non-static requests.""" def __init__(self, app: ASGIApp) -> None: self._app = app async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if scope["type"] != "http": await self._app(scope, receive, send) return path: str = scope.get("path", "") if path.startswith("/static/"): ext = path.rsplit(".", 1)[-1].lower() if "." in path else "" cache_value = _FAR_FUTURE if f".{ext}" in _VERSIONED_EXTS else _ONE_DAY cache_bytes = cache_value.encode() async def _send_static(message: Message) -> None: if message["type"] == "http.response.start": headers: list[tuple[bytes, bytes]] = list(message.get("headers", [])) if not any(k.lower() == b"cache-control" for k, _ in headers): headers.append((b"cache-control", cache_bytes)) message = {**message, "headers": headers} await send(message) await self._app(scope, receive, _send_static) return if path.startswith("/releases/"): async def _send_releases(message: Message) -> None: if message["type"] == "http.response.start": headers = list(message.get("headers", [])) if not any(k.lower() == b"cache-control" for k, _ in headers): # Non-2xx (e.g. 404) must not be cached — prevents edge # caches from locking out a tarball that is added later. status: int = message.get("status", 200) cc = _ONE_HOUR if 200 <= status < 300 else _NO_STORE headers.append((b"cache-control", cc.encode())) message = {**message, "headers": headers} await send(message) await self._app(scope, receive, _send_releases) return await self._app(scope, receive, send)