{"record":{"id":"225243cc59ee9496","repo":"toeverything/AFFiNE","slug":"action-forbidden-225243","errorCode":"action_forbidden","errorMessage":"Only available when avatar storage provider is fs or assetpack.","messagePattern":"Only available when avatar storage provider is fs or assetpack\\.","errorType":"exception","errorClass":"ActionForbidden","httpStatus":403,"severity":"warning","filePath":"packages/backend/server/src/core/user/controller.ts","lineNumber":21,"sourceCode":"\nimport {\n  ActionForbidden,\n  applyAttachHeaders,\n  UserAvatarNotFound,\n} from '../../base';\nimport { Public } from '../auth/guard';\nimport { AvatarStorage } from '../storage';\n\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,","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/user/controller.ts#L3-L39","documentation":"Thrown by UserAvatarController.getAvatar() (GET /api/avatars/:id) when the configured storage provider is neither 'fs' nor 'assetpack'. The route only serves avatars from local filesystem or assetpack storage; other providers (e.g. r2, s3) are expected to serve avatars directly via URL and are not handled by this endpoint.","triggerScenarios":"Client requests GET /api/avatars/:id while server config storage.provider is set to a value outside ['assetpack','fs'] (e.g. 'r2', 's3').","commonSituations":"Deployment switched avatar storage to an object-store provider but the front-end still builds avatar URLs hitting /api/avatars/:id; misconfiguration where provider is unset or a typo.","solutions":["Switch the front-end to use the avatarUrl field returned by the user profile/graphql instead of constructing /api/avatars/:id URLs when provider is object storage.","If local serving is required, set storage.provider to 'fs' in config.","Audit config to ensure the provider value matches the serving strategy."],"exampleFix":"// before — always hitting the REST endpoint\n<img src={`/api/avatars/${user.id}`} />\n\n// after — prefer the stored avatarUrl\n<img src={user.avatarUrl ?? defaultAvatar} />","handlingStrategy":"type-guard","validationCode":"const provider = await fetchStorageProvider();\nif (!['fs', 'assetpack'].includes(provider)) {\n  // use avatarUrl directly, do not call /api/avatars\n}","typeGuard":"function isRestAvatarProvider(provider: string): boolean {\n  return provider === 'fs' || provider === 'assetpack';\n}","tryCatchPattern":"try {\n  await fetch('/api/avatars/' + id);\n} catch (e) {\n  if (e?.code === 'action_forbidden') { useAvatarUrlFieldInstead(); return; }\n  throw e;\n}","preventionTips":["Prefer user.avatarUrl over constructing /api/avatars/:id URLs.","Only call the REST avatar endpoint when the provider is fs/assetpack.","Keep storage.provider config aligned with the client's avatar-URL strategy."],"tags":["user","storage","avatar","config"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}