fix(avatar): revalidate /avatars with ETag so changes appear promptly (LC-348) #366
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/lc-348-avatar-etag-revalidation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes LC-348. Follow-up to LC-347.
Problem
After an avatar upload the new image appeared immediately on the settings page (LC-347 cache-busts that preview with
?v={mtime}), but everywhere else - chat author rows, sidebar, hovercards, voice grid - it kept showing the old image (or the previously-cached default "L" initial) for up to ~5 minutes. Those surfaces render the bare/avatars/{id}URL, androutes::avatar::get_avatarservedCache-Control: public, max-age=300, so the browser held the stale copy for the whole TTL.Fix
get_avatarnow servesCache-Control: no-cacheplus anETag, and answers a matchingIf-None-Matchwith304:dso it can never collide with a file ETag; switching from default to real avatar always busts.A changed avatar is delivered on the next request across every surface; unchanged ones cost only a tiny
304. One route change, no per-template version threading.Tests
New
server/tests/routes_avatar_cache.rs:default_avatar_revalidates_with_etag- default SVG carries ad-prefixed ETag +no-cache; matchingIf-None-Match304s.uploaded_avatar_serves_etag_then_304- uploaded avatar serves a non-default ETag withno-cache; matching tag 304s; a stale tag returns 200 with the fresh image.just testandjust test-saasboth green.Out of scope
Truly refresh-free updates (re-rendering other viewers' message rows over the WS on
UserProfileChanged) remain the separate LC-173 cross-surface follow-up. This issue covers fresh-on-next-request correctness.Follow-up to LC-347. After an avatar upload the new image showed immediately on the settings page (cache-busted with ?v=mtime), but everywhere else - chat author rows, sidebar, hovercards, voice grid - it kept showing the old image (or the previously-cached default initial) for up to five minutes. Those surfaces all render the bare `/avatars/{id}` URL, and the route served `Cache-Control: public, max-age=300`, so the browser held the stale copy for the whole TTL. `get_avatar` now serves `Cache-Control: no-cache` plus an `ETag` and answers a matching `If-None-Match` with `304`. The ETag is the file's extension + length + mtime for a stored avatar, and a content hash (prefixed `d`) for the generated default SVG, so it moves whenever the image changes and the switch from default to real avatar always busts. A changed avatar is therefore delivered on the next request across every surface; unchanged ones cost only a tiny `304`. Adds `routes_avatar_cache.rs`: the default SVG revalidates (304 on matching tag), an uploaded avatar serves a non-default ETag with `no-cache`, a matching `If-None-Match` 304s, and a stale tag returns 200 with the fresh image. Truly refresh-free updates (re-rendering other viewers' message rows over the WS on UserProfileChanged) remain the separate LC-173 cross-surface follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>