{"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":"info","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/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/user/controller.ts#L11-L46","documentation":"Thrown by UserAvatarController.getAvatar() after storage.get(id) returns an empty body. The provider check passed (fs/assetpack) but no blob exists for the given id, so there is nothing to stream.","triggerScenarios":"GET /api/avatars/:id where the storage backend has no object for that id — avatar was never uploaded, was deleted, or the id is wrong.","commonSituations":"User never set an avatar but the client still requests one; avatar was removed (removeAvatar mutation) and the client cached the old URL; id mismatch after a storage migration.","solutions":["Default to a generated/fallback avatar on the client when this 404 is returned.","Clear cached avatar URLs when the user removes their avatar.","Verify the id being requested matches the storage key format `${userId}-avatar-${timestamp}`."],"exampleFix":"// before\n<img src={`/api/avatars/${id}`} />\n\n// after — handle 404 with fallback\n<img src={avatarUrl} onError={(e) => { e.currentTarget.src = fallbackAvatar(id); }} />","handlingStrategy":"fallback","validationCode":"const exists = await checkAvatarExists(id);\nif (!exists) return fallbackAvatar(id);","typeGuard":"function hasAvatar(user?: { avatarUrl?: string | null }): boolean {\n  return Boolean(user?.avatarUrl);\n}","tryCatchPattern":"try {\n  await fetch('/api/avatars/' + id);\n} catch (e) {\n  if (e?.code === 'user_avatar_not_found') { setFallbackAvatar(); return; }\n  throw e;\n}","preventionTips":["Render avatars from user.avatarUrl and fall back to a generated avatar on error.","Clear cached avatar URLs when removeAvatar succeeds.","Treat the 404 as expected for users who never set an avatar."],"tags":["user","storage","avatar","not-found"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}