{"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/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/user/controller.ts#L3-L39","documentation":"GET /api/avatars/:id can only stream bytes when avatars live on the local filesystem or in the bundled asset pack (storage provider 'fs' or 'assetpack'). With remote providers, avatars are served from the provider's own URL, so this endpoint is intentionally disabled and answers action_forbidden before touching storage.","triggerScenarios":"Calling /api/avatars/:id while the storage provider is configured to anything other than 'fs' or 'assetpack' (e.g. s3/r2/azure); integrations hardcoding /api/avatars URLs after the deployment switched providers.","commonSituations":"Self-hosted instance migrated to S3-compatible storage while clients or bookmarks still hit the avatar API; scripts assuming the route exists on every deployment type.","solutions":["Use the avatarUrl field on the user object - with remote providers it already points at the provider-served URL","If API-served avatars are required, switch the storage provider back to 'fs' or 'assetpack'","Stop hand-building /api/avatars/... URLs in clients and integrations"],"exampleFix":"// before\nconst url = `/api/avatars/${avatarKey}`; // 403 when provider is s3/r2\n\n// after\nconst url = user.avatarUrl ?? `/api/avatars/${avatarKey}`;","handlingStrategy":"fallback","validationCode":"// prefer the stored avatar URL; only call the API for local providers\nfunction avatarSrc(user: { avatarUrl: string | null }): string {\n  return user.avatarUrl ?? defaultAvatar;\n}","typeGuard":"function isApiServedAvatar(avatarUrl: string): boolean {\n  return avatarUrl.startsWith('/api/avatars/');\n}","tryCatchPattern":"try {\n  return await fetch(`/api/avatars/${id}`);\n} catch (e) {\n  if (e?.code === 'action_forbidden') return fetch(user.avatarUrl); // provider-served URL\n  throw e;\n}","preventionTips":["Never construct /api/avatars URLs yourself - always read user.avatarUrl","Know your deployment's storage provider: remote providers serve avatars directly from object storage","Update integrations and bookmarks after migrating storage providers"],"tags":["storage","avatars","configuration","http"],"backgroundTag":"storage-provider-misconfigured","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"}