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

  1. 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.
  2. If local serving is required, set storage.provider to 'fs' in config.
  3. 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

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


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/225243cc59ee9496. Report an issue: GitHub.