{"record":{"id":"e36a80857c121da5","repo":"toeverything/AFFiNE","slug":"user-avatar-not-found","errorCode":"user_avatar_not_found","errorMessage":"User avatar not found.","messagePattern":"User avatar not found\\.","errorType":"exception","errorClass":"UserAvatarNotFound","httpStatus":404,"severity":"error","filePath":"packages/backend/server/src/core/user/controller.ts","lineNumber":29,"sourceCode":"\n@Public()\n@Controller('/api/avatars')\nexport class UserAvatarController {\n  constructor(private readonly storage: AvatarStorage) {}\n\n  @Get('/:id')\n  async getAvatar(@Res() res: Response, @Param('id') id: string) {\n    const provider = this.storage.config.storage.provider;\n    if (!['assetpack', 'fs'].includes(provider)) {\n      throw new ActionForbidden(\n        'Only available when avatar storage provider is fs or assetpack.'\n      );\n    }\n\n    const { body, metadata } = await this.storage.get(id);\n\n    if (!body) {\n      throw new UserAvatarNotFound();\n    }\n\n    // metadata should always exists if body is not null\n    if (metadata) {\n      res.setHeader('content-type', metadata.contentType);\n      res.setHeader('last-modified', metadata.lastModified.toISOString());\n      res.setHeader('content-length', metadata.contentLength);\n    }\n    applyAttachHeaders(res, {\n      contentType: metadata?.contentType,\n      filename: `${id}`,\n    });\n\n    body.pipe(res);\n  }\n}\n","sourceCodeStart":11,"sourceCodeEnd":46,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/user/controller.ts#L11-L46","documentation":"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.","triggerScenarios":"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.","commonSituations":"Cached <img> URLs pointing at a replaced avatar; races right after avatar update/remove; crawlers enumerating ids.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// key caches by avatarUrl and treat absence as 'no avatar'\nconst src = user.avatarUrl ?? placeholderAvatar;","typeGuard":null,"tryCatchPattern":"try {\n  await loadAvatar(avatarUrl);\n} catch (e) {\n  if (e?.code === 'user_avatar_not_found') showInitialsFallback();\n  else throw e;\n}","preventionTips":["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)"],"tags":["http","avatars","not-found","storage"],"backgroundTag":"resource-not-found","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}