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

  1. Refetch the user profile and request the current avatarUrl instead of a cached id
  2. Treat the 404 as 'no avatar' and render a fallback (initials or placeholder)
  3. 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

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)