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
- Sign in as a workspace member with Workspace.Read (owner/admin/member)
- In workspace settings, enable blob sharing and keep at least one doc publicly shared so canReadSharedWorkspaceBlobs passes
- Refresh expired auth tokens/cookies before fetching blob URLs
- 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
- Fetch blob URLs through the authenticated client, not raw hotlinks
- Refresh sessions before rendering embedded blob content
- If embedding externally, ensure workspace blob sharing + a public doc exist
- On membership loss, stop reusing previously working blob URLs
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)