gabriel / musehub public
static_cache.py python
71 lines 3.1 KB
Raw
sha256:1dd27e6d5c24550177b01b0a171e8375c07cf046b549e7683364706b6522d3bc docs(#139): mark all 11 checklist items resolved, document … Sonnet 5 12 days ago
1 """Static asset Cache-Control middleware.
2
3 Injects ``Cache-Control`` headers on ``/static/`` and ``/releases/`` responses:
4
5 - ``/static/*.css``, ``/static/*.js``, ``/static/*.map`` — far-future 1-year
6 immutable (paired with ``?v=<hash8>`` query strings in ``base.html``).
7 - Other ``/static/`` paths — 1-day TTL (un-versioned assets like favicons).
8 - ``/releases/*.tar.gz`` — 1-hour TTL for sdist tarballs; short enough for
9 republishing, long enough to reduce origin load during install waves.
10 - Any non-2xx response on ``/releases/`` — ``no-store`` so Cloudflare never
11 caches 404s (which would break installs if a tarball is added after first
12 access).
13 """
14
15 from starlette.types import ASGIApp, Message, Receive, Scope, Send
16
17 _FAR_FUTURE = "public, max-age=31536000, immutable"
18 _ONE_DAY = "public, max-age=86400"
19 _ONE_HOUR = "public, max-age=3600"
20 _NO_STORE = "no-store"
21
22 # Extensions that always carry a ?v= query string in base.html.
23 # All other /static/ paths get the shorter 1-day TTL.
24 _VERSIONED_EXTS = frozenset([".css", ".js", ".map"])
25
26 class StaticCacheMiddleware:
27 """Pure ASGI middleware — zero overhead for non-static requests."""
28
29 def __init__(self, app: ASGIApp) -> None:
30 self._app = app
31
32 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
33 if scope["type"] != "http":
34 await self._app(scope, receive, send)
35 return
36
37 path: str = scope.get("path", "")
38
39 if path.startswith("/static/"):
40 ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
41 cache_value = _FAR_FUTURE if f".{ext}" in _VERSIONED_EXTS else _ONE_DAY
42 cache_bytes = cache_value.encode()
43
44 async def _send_static(message: Message) -> None:
45 if message["type"] == "http.response.start":
46 headers: list[tuple[bytes, bytes]] = list(message.get("headers", []))
47 if not any(k.lower() == b"cache-control" for k, _ in headers):
48 headers.append((b"cache-control", cache_bytes))
49 message = {**message, "headers": headers}
50 await send(message)
51
52 await self._app(scope, receive, _send_static)
53 return
54
55 if path.startswith("/releases/"):
56 async def _send_releases(message: Message) -> None:
57 if message["type"] == "http.response.start":
58 headers = list(message.get("headers", []))
59 if not any(k.lower() == b"cache-control" for k, _ in headers):
60 # Non-2xx (e.g. 404) must not be cached — prevents edge
61 # caches from locking out a tarball that is added later.
62 status: int = message.get("status", 200)
63 cc = _ONE_HOUR if 200 <= status < 300 else _NO_STORE
64 headers.append((b"cache-control", cc.encode()))
65 message = {**message, "headers": headers}
66 await send(message)
67
68 await self._app(scope, receive, _send_releases)
69 return
70
71 await self._app(scope, receive, send)
File History 2 commits
sha256:1dd27e6d5c24550177b01b0a171e8375c07cf046b549e7683364706b6522d3bc docs(#139): mark all 11 checklist items resolved, document … Sonnet 5 12 days ago
sha256:649011bedd713e22f7dca4c4be94bdefbf3b10950d9fc80235928d0b0b823be8 docs: track issue #139 (two-column app-shell refactor) plan… Sonnet 5 12 days ago