toeverything/AFFiNE · warning · ActionForbidden
action_forbidden
action_forbidden
Error message
Only available when avatar storage provider is fs or assetpack.
What it means
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.
Source
Thrown at packages/backend/server/src/core/user/controller.ts:21
import {
ActionForbidden,
applyAttachHeaders,
UserAvatarNotFound,
} from '../../base';
import { Public } from '../auth/guard';
import { AvatarStorage } from '../storage';
@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,View on GitHub (pinned to 26c515e050)
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.
Example fix
// before — always hitting the REST endpoint
<img src={`/api/avatars/${user.id}`} />
// after — prefer the stored avatarUrl
<img src={user.avatarUrl ?? defaultAvatar} /> Defensive patterns
Strategy: type-guard
Validate before calling
const provider = await fetchStorageProvider();
if (!['fs', 'assetpack'].includes(provider)) {
// use avatarUrl directly, do not call /api/avatars
} Type guard
function isRestAvatarProvider(provider: string): boolean {
return provider === 'fs' || provider === 'assetpack';
} Try / catch
try {
await fetch('/api/avatars/' + id);
} catch (e) {
if (e?.code === 'action_forbidden') { useAvatarUrlFieldInstead(); return; }
throw e;
} Prevention
- 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.
When it happens
Trigger: Client requests GET /api/avatars/:id while server config storage.provider is set to a value outside ['assetpack','fs'] (e.g. 'r2', 's3').
Common situations: 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.
Related errors
- user_avatar_not_found
- user_not_found
- image_format_not_supported
- ErrorCode.ValueNotExists
- Invalid config for module [${module}] with key [${key}] Valu
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/225243cc59ee9496.
Report an issue: GitHub.