toeverything/AFFiNE · error · DocActionDenied
doc_action_denied
doc_action_denied
Error message
You do not have permission to perform ${action} action on doc ${docId}. What it means
Thrown by WorkspacesController.assertCanReadPublicDoc when the acting user fails the Doc.Read capability check on (workspaceId, docId) — this.ac.user(userId).doc(...).can('Doc.Read') returned false. It guards shared/public-doc endpoints that still require at least read access, and carries docId, spaceId, and the failed action ('Doc.Read') in the error payload.
Solutions
- Share the doc with the user or publish it publicly (workspace/doc sharing settings) so Doc.Read evaluates true
- Verify the URL pairs the correct workspaceId and docId — a mismatched pair fails the check
- Authenticate as a workspace member (owner/admin/member) before requesting the doc
- Server-side, confirm the docAccessPolicy/sharing rows exist for that doc — they are what let outsiders pass Doc.Read
Defensive patterns
Strategy: try-catch
Validate before calling
// before requesting a shared doc, confirm read access if your client has a capability probe
const canRead = await graphql(`
query CanRead($wsId: String!, $docId: String!) {
workspace(id: $wsId) { doc(id: $docId) { canRead } }
}
`, { wsId, docId });
if (!canRead) {
showAccessRequest(wsId, docId);
return;
} Type guard
function isDocActionDenied(e: unknown): e is { code: 'doc_action_denied'; docId: string; spaceId: string; action: string } {
return typeof e === 'object' && e !== null && (e as any).code === 'doc_action_denied';
} Try / catch
try {
return await fetchPublicDoc(wsId, docId);
} catch (e) {
if (isDocActionDenied(e)) {
return renderNoAccessPage(e.docId); // 403: do not retry with same identity
}
throw e;
} Prevention
- Before deep-linking, verify the doc is shared with the current user (or public)
- Keep auth tokens fresh so requests don't degrade to anonymous and lose Doc.Read
- On permission revocation webhooks/events, purge cached doc links from the UI
- Always pair workspaceId and docId from the same source (doc list), never mix
When it happens
Trigger: Hitting endpoints that funnel through assertCanReadPublicDoc (e.g. public doc reads and the comment-attachment route) for a doc that is not shared with the caller and where the caller has no workspace member role granting Doc.Read.
Common situations: Opening a link to a doc whose public sharing was revoked or never enabled; logged-out visitors hitting member-only docs; wrong workspaceId/docId pair (doc exists in another space); membership or doc-access-policy rows not yet visible after a role change.
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
- space_access_denied
- can_not_batch_grant_doc_owner_permissions
- can_not_batch_grant_doc_owner_permissions
- mention_user_doc_access_denied
- space_access_denied
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/907b1016104fa276.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/workspaces/controller.ts:67
private buildVisitorId(req: Request, workspaceId: string, docId: string) {
const tracker = getRequestTrackerId(req);
return createHash('sha256')
.update(`${workspaceId}:${docId}:${tracker}`)
.digest('hex');
}
private async assertCanReadPublicDoc(
userId: string,
workspaceId: string,
docId: string
) {
const canReadSharedDoc = await this.ac
.user(userId)
.doc(workspaceId, docId)
.can('Doc.Read');
if (!canReadSharedDoc) {
throw new DocActionDenied({
docId,
spaceId: workspaceId,
action: 'Doc.Read',
});
}
}
private async getPublishModeHeader(workspaceId: string, docId: string) {
const docMeta = await this.models.doc.getMeta(workspaceId, docId, {
select: {
mode: true,
},
});
return docMeta?.mode === PublicDocMode.Edgeless
? DocMode.edgeless
: DocMode.page;
}
View on GitHub (pinned to 2af30773ae)