fix(webmail): a new profile photo shows up everywhere at once

The photo endpoint was cached for five minutes with no revalidation, and only
the settings page added a version to the URL, so the header, the message list
and the reader kept the old photo until it expired.

- every <Avatar> reads a per-address version (and refetches when it changes);
  saving or removing a photo bumps it, in this tab and the other open tabs
- the server answers private, max-age=60, must-revalidate, so a stale copy
  lives for a minute at most and then costs one 304

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-16 16:44:14 -03:00
1 parent d82cf71f5b
commit c0f556b190
6 files changed
+146 -4

No files matched your search

+8
View File
@@ -946,6 +946,14 @@ describeDb("auth API against a real database", () => {
expect(got.headers.get("content-type")).toBe("image/png");
expect(Buffer.from(await got.arrayBuffer())).toEqual(png);
// A new photo has to show up quickly: the browser may reuse the old one
// for at most a minute, and must ask again after that. It was five minutes
// with no revalidation, which is what made a changed photo "take forever".
const cacheControl = got.headers.get("cache-control") ?? "";
expect(cacheControl).toContain("private");
expect(cacheControl).toContain("must-revalidate");
expect(Number(/max-age=(\d+)/.exec(cacheControl)?.[1])).toBeLessThanOrEqual(60);
// A photo appears beside every message from that person; without the
// conditional answer it would be re-sent on every one of them.
const etag = got.headers.get("etag")!;
+4 -1
View File
@@ -343,7 +343,10 @@ async function getAvatar(
res.setHeader("ETag", `"${avatar.etag}"`);
// Private: this is served to a signed-in reader, and a shared cache must not
// hand one person's photo to the next request that comes along.
res.setHeader("Cache-Control", "private, max-age=300");
// One minute, then revalidate with the ETag (a 304 costs no body). It was five
// minutes with no revalidation, so a new photo took that long to show up
// wherever the page did not add a version to the URL.
res.setHeader("Cache-Control", "private, max-age=60, must-revalidate");
// The bytes were sniffed on the way in, but a browser that ignores the type
// and sniffs for itself must not be allowed to find something else.
res.setHeader("X-Content-Type-Options", "nosniff");