Files
buzz/crates/buzz-media
John Matthew Tennant 1bb2ad443d fix(relay): regenerate missing image thumbnails on request (#9174)
## Summary

Clients ask for an image's small `.thumb.webp` thumbnail first and fall
back to `.thumb.jpg` on a 404. Images uploaded before
[#9003](https://github.com/block/buzz/pull/9003) only have the JPEG, so
every client pays an extra 404 for them and transparent avatars keep
showing on a black background. The same thing happens when a thumbnail
write failed at upload time.

When a thumbnail variant is missing but its image original exists, the
relay now regenerates the thumbnails on that request and serves them
with 200. It uses the same generator and storage writes as upload, which
are now one shared `store_image_thumbnails`. Later requests are ordinary
hits.

- GET (full and range) and HEAD both regenerate, so a HEAD-then-GET
client sees the same result.
- Regeneration runs only after the existing tenant, auth and sidecar
checks for that thumbnail.
- It is bounded. At most two regenerations run per relay process
(`try_acquire`, no waiting), and originals larger than `max_image_bytes`
are skipped. In both cases, and for non-images, missing originals or any
decode or storage error, the response is the 404 it is today. Errors are
logged at warn.

### Related issue

Follow-up to [#9003](https://github.com/block/buzz/pull/9003) and
[#9105](https://github.com/block/buzz/pull/9105), which made relays
write WebP thumbnails and clients prefer them.

### Testing

Two router-level tests against Postgres and MinIO upload a PNG through
the real route, then delete its thumbnails:

- `missing_thumbnail_is_regenerated_from_image_original`: GET and HEAD
of `.thumb.webp` and `.thumb.jpg` return 200 with the right content
type, and both objects are stored afterward. With regeneration disabled
it fails with `GET webp: 404, expected 200`.
- `missing_thumbnail_is_not_regenerated_when_unsafe`: a non-image blob,
a missing original, an oversize original and a busy relay (no free
permit) each return 404 and store nothing. Once the permits are free,
the same request returns 200. Removing the size cap or the permit guard
fails it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: John Tennant <jtennant@squareup.com>
Co-authored-by: Simone <0828be588d5c9c4d092c5b1cf01761d5aef63d6b104807fd8e8284d517dd1a99@buzz.block.builderlab.xyz>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 11:46:27 -05:00
..