toeverything/AFFiNE · error · SpaceAccessDenied

space_access_denied

space_access_denied

Error message

You do not have permission to access Space ${spaceId}.

What it means

Thrown by WorkspacesController.blob (GET /api/workspaces/:id/blobs/:name) when the caller has neither Workspace.Read on the space nor shared-workspace blob access — canReadSharedWorkspaceBlobs requires models.workspace.allowSharing(workspaceId) AND docAccessPolicy.hasPublicExternal(workspaceId). The error deliberately hides blob existence from non-members.

Solutions

  1. Sign in as a workspace member with Workspace.Read (owner/admin/member)
  2. In workspace settings, enable blob sharing and keep at least one doc publicly shared so canReadSharedWorkspaceBlobs passes
  3. Refresh expired auth tokens/cookies before fetching blob URLs
  4. Fetch blobs through the authenticated client instead of raw public links
Defensive patterns

Strategy: try-catch

Type guard

function isSpaceAccessDenied(e: unknown): e is { code: 'space_access_denied'; spaceId: string } {
  return typeof e === 'object' && e !== null && (e as any).code === 'space_access_denied';
}

Try / catch

try {
  return await fetchBlob(wsId, name);
} catch (e) {
  if (isSpaceAccessDenied(e)) {
    if (!isAuthenticated()) return reauthenticateAndRetry(); // anonymous may be the cause
    return renderPrivateBlobPlaceholder();
  }
  throw e;
}

Prevention

When it happens

Trigger: Fetching a blob URL while logged out or as a non-member on a workspace that has not enabled blob sharing alongside publicly shared docs; expired session so the user resolves to 'anonymous'.

Common situations: Hotlinked/embedded blob images on external sites after sharing was disabled; users reopening old image links after removal from the workspace; frontend requests sent without refreshed auth cookies.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/455ab47be60d4e40. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/workspaces/controller.ts:117

  // NOTE: because graphql can't represent a File, so we have to use REST API to get blob
  @Public()
  @Get('/:id/blobs/:name')
  @CallMetric('controllers', 'workspace_get_blob')
  async blob(
    @CurrentUser() user: CurrentUser | undefined,
    @Param('id') workspaceId: string,
    @Param('name') name: string,
    @Query('redirect') redirect: string | undefined,
    @Res() res: Response
  ) {
    const canReadWorkspace = await this.ac
      .user(user?.id ?? 'anonymous')
      .workspace(workspaceId)
      .can('Workspace.Read');
    const canReadSharedWorkspaceBlobs =
      await this.canReadSharedWorkspaceBlobs(workspaceId);
    if (!canReadWorkspace && !canReadSharedWorkspaceBlobs) {
      throw new SpaceAccessDenied({ spaceId: workspaceId });
    }
    const { body, metadata, redirectUrl } = await this.storage.get(
      workspaceId,
      name,
      true
    );

    if (redirectUrl) {
      // redirect to signed url
      if (redirect === 'manual') {
        return res.send({
          url: redirectUrl,
        });
      } else {
        return res.redirect(redirectUrl);
      }
    }

View on GitHub (pinned to b4c8548c09)