Skip to content

Webhook avatars were stored full size and never resized (GRYT-1185) - #191

Merged
sivert-io merged 2 commits into
mainfrom
claude/GRYT-1185-webhook-avatar-resize
Sep 15, 2026
Merged

sivert-io merged 2 commits into
mainfrom
claude/GRYT-1185-webhook-avatar-resize

Conversation

@sivert-io

@sivert-io sivert-io commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

Nothing here is in a review-required path. src/db, src/storage, src/middleware and src/auth aren't touched. The change only calls insertFile, putObject and ensurePermission as they are.

Vikunja: GRYT-1185. Client: Gryt-chat/client#569. Docs: Gryt-chat/docs#115.

What was wrong

The webhook settings tab uploaded its avatar through POST /api/uploads. That's the attachment route, and it keeps a picture as it was sent. A 2400×1600 PNG sent through it is stored as a 5.9 MB PNG with no thumbnail. The chat view draws the webhook's avatar from that full file. A per-message avatar_url from server#187 was stored the same way, as a card picture.

What changed

  • POST /api/uploads/webhook-avatar runs storeAvatarImage("webhook"), the same handler as the avatar and group icon routes. It needs manage_webhooks, like every other webhook route. It replies { fileId, processing } and doesn't write the user row.
  • The raster half of that handler moved into src/services/avatarImage.ts as storeAvatarPicture, so the webhook post route can call it too. The first commit is only that move. git diff --color-moved on it shows the moved lines. The other edits make it return a result instead of writing the response.
  • avatar_url on a webhook post goes through storeAvatarPicture now. Card pictures are stored as sent, like before.
  • The avatar_url description in openapi/webhooks.json says it's resized.

Please look at

  • The webhook_media key. A per-message avatar is deduplicated under avatar:<sha256> instead of the bare hash. Otherwise the same bytes in a card could get handed the resized copy. It's a plain string in the sha256 column, and that column has no format check, so there's no schema change.
  • One download, two files. When avatar_url and a card icon use the same URL, it's still downloaded once, then stored twice: resized for the avatar, as sent for the icon. If one store fails, only that slot gets store_failed.
  • Older clients still upload through /api/uploads, and the server still accepts that file as a webhook avatar. I didn't add a check for it.
  • Per-message avatars no longer queue an image-worker job. Member avatars never did.

File access and the sweep

Tests

  • webhookAvatar.test.ts uploads through the new route now, and adds:
    • a 1600×1200 PNG is stored as a 256×256 AVIF under avatars/ with a 128 px thumbnail, read back from storage
    • SVG leaves your avatar alone too
    • a member without manage_webhooks gets 403 and nothing is stored
    • the file is only the uploader's to read until a webhook wears it
    • a post's avatar_url is stored resized, while the same picture in a card isn't
    • a webhook sending the same avatar twice reuses one file
  • webhookMedia.test.ts: the avatar and an icon from the same URL are one download and two stores, and a failed avatar store leaves the icon alone.
  • uploadAuthOrder.test.ts lists the new route.
  • I mutation-checked them. Dropping the avatar flag on avatar_url, writing the user row for webhook uploads, sharing the dedupe key, the wrong permission, and mixing up the per-slot file id each fail at least one test.
  • yarn test (1292 tests), yarn test:examples, yarn build, npx eslint . and the comment check pass locally.

Verified

Throwaway server on this branch with filesystem storage, the client branch, headless Chrome:

  • Uploading a 5.9 MB 2400×1600 PNG from the webhooks tab stored a 599-byte 256×256 AVIF and a 428-byte 128×128 thumbnail. My own avatar_file_id stayed null.
  • A message posted to the webhook showed that avatar, loaded at 256×256.
  • A message whose avatar_url pointed at a 644 KB 1200×630 PNG on raw.githubusercontent.com stored a 1.9 KB 256×256 AVIF for the avatar. The same URL as the card's image_url stayed the 644 KB PNG.
  • The same 5.9 MB PNG sent to POST /api/uploads is still stored as it was sent, which is what the tab did before.
  • With main's server on the same data, the route answers 404 and the client shows its update message. That part is in the client PR.

🤖 Generated with Claude Code

sivert-io and others added 2 commits September 15, 2026 15:51
The avatar a webhook post carries needs the same processing as a member's
avatar. That code only lived inside the upload route's handler. It moves out
as it was, except it returns a result now instead of writing the response.
The SVG path, the size limit and the user row stay in the route.

No behaviour change. `git diff --color-moved` shows the moved lines.

Part of GRYT-1185.

Co-Authored-By: Claude Opus 5 <[email protected]>
The webhook settings tab uploaded its avatar through POST /api/uploads, the
attachment route. So it was stored at whatever size it was sent. A per-message
avatar_url went the same way, as a card picture. Both now go through the same
pipeline as member avatars and group pictures. A still picture ends up as a
256 px AVIF under avatars/, with a 128 px thumbnail.

- New POST /api/uploads/webhook-avatar, which needs manage_webhooks. It's
  shaped like the group icon route and replies { fileId, processing }. It
  leaves the uploader's own avatar alone.
- avatar_url goes through storeAvatarPicture. It gets its own key in
  webhook_media, so the same bytes in a card still get their own file, stored
  as sent. The URL is still only downloaded once.
- The avatar_url description in the OpenAPI document says it's resized.

Co-Authored-By: Claude Opus 5 <[email protected]>
@sivert-io
sivert-io merged commit dfeb0f8 into main Sep 15, 2026
3 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-1185-webhook-avatar-resize branch September 15, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant