Skip to content

Commit 191ba51

Browse files
committed
Turn repeated requests for unhashed URLs into 304 Not Modified
Unhashed URL responses now include an ETag header derived from the content hash already computed at startup. When a browser sends If-None-Match with a matching ETag, the server responds with 304 and an empty body instead of re-sending the file. Hashed URLs are unaffected since they already use Cache-Control: immutable. Key design decisions: - ETags are pre-computed as quoted byte strings in _hash_files() alongside file_map and _reverse, so serving adds zero computation. - Only unhashed URLs get ETags. Hashed URLs have the hash baked into the filename itself, making ETags redundant.
1 parent 2c3ef7b commit 191ba51

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

src/staticware/middleware.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ def __init__(
6565
self.file_map: dict[str, str] = {}
6666
# hashed relative path -> original relative path
6767
self._reverse: dict[str, str] = {}
68+
# original relative path -> ETag value (quoted hash)
69+
self._etags: dict[str, bytes] = {}
6870

6971
self._hash_files()
7072

@@ -100,6 +102,7 @@ def _hash_files(self) -> None:
100102

101103
self.file_map[relative] = hashed
102104
self._reverse[hashed] = relative
105+
self._etags[relative] = f'"{hash_val}"'.encode('latin-1')
103106

104107
def url(self, path: str) -> str:
105108
"""Return the cache-busted URL for a static file path.
@@ -155,7 +158,18 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
155158
await _send_text(send, 404, b"Not Found")
156159
return
157160
if file_path.exists() and file_path.is_file():
158-
await _send_file(send, file_path)
161+
etag = self._etags.get(relative_path)
162+
if etag:
163+
# Check for conditional request (If-None-Match)
164+
for hdr_name, hdr_value in scope.get("headers", []):
165+
if hdr_name == b"if-none-match" and hdr_value == etag:
166+
await _send_text(send, 304, b"")
167+
return
168+
await _send_file(
169+
send, file_path, extra_headers=[(b"etag", etag)]
170+
)
171+
else:
172+
await _send_file(send, file_path)
159173
return
160174

161175
await _send_text(send, 404, b"Not Found")

tests/test_staticware.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,70 @@ async def test_serve_with_mismatched_mount_and_prefix(static_dir: Path) -> None:
461461
await static(scope, receive, resp)
462462
assert resp.status == 200
463463
assert resp.text == "body { color: red; }"
464+
465+
466+
# ── HashedStatic: ETag and conditional requests ──────────────────────
467+
468+
469+
def make_scope_with_headers(
470+
path: str, headers: list[tuple[bytes, bytes]] | None = None
471+
) -> dict[str, Any]:
472+
scope: dict[str, Any] = {"type": "http", "path": path, "method": "GET"}
473+
if headers:
474+
scope["headers"] = headers
475+
return scope
476+
477+
478+
async def test_etag_on_unhashed_response(
479+
static: HashedStatic, static_dir: Path
480+
) -> None:
481+
"""Original filename response includes an ETag header with the content hash."""
482+
resp = ResponseCollector()
483+
await static(make_scope("/static/styles.css"), receive, resp)
484+
assert resp.status == 200
485+
486+
css_content = (static_dir / "styles.css").read_bytes()
487+
h = expected_hash(css_content)
488+
assert b"etag" in resp.headers, "Response should include an etag header"
489+
assert resp.headers[b"etag"] == f'"{h}"'.encode("latin-1")
490+
491+
492+
async def test_conditional_request_returns_304(
493+
static: HashedStatic, static_dir: Path
494+
) -> None:
495+
"""If-None-Match with matching ETag returns 304 and empty body."""
496+
css_content = (static_dir / "styles.css").read_bytes()
497+
h = expected_hash(css_content)
498+
etag_value = f'"{h}"'.encode("latin-1")
499+
500+
scope = make_scope_with_headers(
501+
"/static/styles.css",
502+
headers=[(b"if-none-match", etag_value)],
503+
)
504+
resp = ResponseCollector()
505+
await static(scope, receive, resp)
506+
assert resp.status == 304
507+
assert resp.body == b""
508+
509+
510+
async def test_conditional_request_mismatched_etag_returns_200(
511+
static: HashedStatic,
512+
) -> None:
513+
"""If-None-Match with wrong ETag returns 200 with full body."""
514+
scope = make_scope_with_headers(
515+
"/static/styles.css",
516+
headers=[(b"if-none-match", b'"wronghash"')],
517+
)
518+
resp = ResponseCollector()
519+
await static(scope, receive, resp)
520+
assert resp.status == 200
521+
assert resp.text == "body { color: red; }"
522+
523+
524+
async def test_hashed_url_no_etag(static: HashedStatic) -> None:
525+
"""Hashed URL responses use immutable caching and should not include an ETag."""
526+
hashed_name = static.file_map["styles.css"]
527+
resp = ResponseCollector()
528+
await static(make_scope(f"/static/{hashed_name}"), receive, resp)
529+
assert resp.status == 200
530+
assert b"etag" not in resp.headers, "Hashed URL should not include an etag header"

0 commit comments

Comments
 (0)