/api/media served every file type with Cache-Control: private, max-age=3600,
so the browser reused a cached response for 1 hour without revalidating. When
an agent overwrites a file in place (same name, new content — e.g. a
regenerated image), inline previews kept showing the stale version until a
manual full-page refresh.
Changes:
- _serve_file_bytes now computes a weak ETag from file metadata
(mtime_ns + size) and honors If-None-Match with a 304 short-circuit
(RFC 7232 weak comparison, W/ prefixes ignored, '*' support), processed
before the Range block since a matched conditional request skips the
entity entirely. 200/206 responses carry the ETag.
- _handle_media splits the cache strategy by MIME: text/html keeps no-store
(inline previews change constantly, PR #6706); all other media (images,
audio, video, PDF) switch from max-age=3600 to no-cache so the browser
MAY cache but MUST revalidate on every use — in-place updates are picked
up immediately while unchanged files still short-circuit with 304.
Why weak ETag: metadata-based equality is sufficient for the overwrite
case and avoids hashing large files per request.
Why no-cache over must-revalidate: no-cache forces revalidation before
every use regardless of freshness; must-revalidate only fires after
max-age elapses, which does not help when the browser believes its cached
copy is still fresh.
Tests: tests/test_media_etag_revalidation.py (15 tests) covering ETag
emission, 304 short-circuit (match/star/mismatch/list/weak-comparison),
ETag change on in-place replacement, Range+ETag interaction, 304-before-
Range precedence, and the _handle_media end-to-end path (no-cache for
images/PDF, no-store preserved for HTML).