toeverything/AFFiNE · error · UserAvatarNotFound
user_avatar_not_found
user_avatar_not_found
Error message
User avatar not found.
What it means
With a supported provider, /api/avatars/:id proxies the stored object; if storage.get(id) returns no body (the object was deleted or never existed) the route answers user_avatar_not_found. Uploading a new avatar deletes the previous object, so old ids go stale immediately.
Solutions
- Refetch the user profile and request the current avatarUrl instead of a cached id
- Treat the 404 as 'no avatar' and render a fallback (initials or placeholder)
- Cache avatar responses keyed by avatarUrl with a short TTL, not by user id
Defensive patterns
Strategy: try-catch
Validate before calling
// key caches by avatarUrl and treat absence as 'no avatar' const src = user.avatarUrl ?? placeholderAvatar;
Try / catch
try {
await loadAvatar(avatarUrl);
} catch (e) {
if (e?.code === 'user_avatar_not_found') showInitialsFallback();
else throw e;
} Prevention
- Always render avatars from the current user.avatarUrl, not a cached id
- Give avatar <img> tags an onerror fallback to initials/placeholder
- Expect old avatar objects to disappear after each avatar replacement (the old object is deleted)
When it happens
Trigger: Requesting an avatar id from before a replacement upload (uploadAvatar deletes user.avatarUrl afterwards); hitting the route after removeAvatar set avatarUrl to null and storage was cleaned; requesting a fabricated or mistyped id.
Common situations: Cached <img> URLs pointing at a replaced avatar; races right after avatar update/remove; crawlers enumerating ids.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/e36a80857c121da5.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/user/controller.ts:29
@Public()
@Controller('/api/avatars')
export class UserAvatarController {
constructor(private readonly storage: AvatarStorage) {}
@Get('/:id')
async getAvatar(@Res() res: Response, @Param('id') id: string) {
const provider = this.storage.config.storage.provider;
if (!['assetpack', 'fs'].includes(provider)) {
throw new ActionForbidden(
'Only available when avatar storage provider is fs or assetpack.'
);
}
const { body, metadata } = await this.storage.get(id);
if (!body) {
throw new UserAvatarNotFound();
}
// metadata should always exists if body is not null
if (metadata) {
res.setHeader('content-type', metadata.contentType);
res.setHeader('last-modified', metadata.lastModified.toISOString());
res.setHeader('content-length', metadata.contentLength);
}
applyAttachHeaders(res, {
contentType: metadata?.contentType,
filename: `${id}`,
});
body.pipe(res);
}
}
View on GitHub (pinned to b4c8548c09)