toeverything/AFFiNE · error · ExpectToRevokePublicDoc

expect_to_revoke_public_doc

expect_to_revoke_public_doc

Error message

Expect doc not to be workspace

What it means

revokePublicDoc rejects calls where workspaceId === docId, mirroring publishDoc: the workspace root is a Space, not a published doc, so revoking 'the workspace id itself' is a client mistake and throws ExpectToRevokePublicDoc before permissions are evaluated.

Solutions

  1. Guard client-side: only call revokePublicDoc when docId !== workspaceId
  2. Fix the data source that produced a docId equal to the workspace id
  3. If the goal was to unpublish everything, revoke each published page individually

Example fix

// before
await revokePublicDoc(ws.id, docIdFromLegacyRecord /* === ws.id */);

// after
if (docId === ws.id) {
  // legacy record pointed at the root; nothing to revoke
  return;
}
await revokePublicDoc(ws.id, docId);
Defensive patterns

Strategy: validation

Validate before calling

// Skip revoke calls that target the root
if (docId !== ws.id) {
  await revokePublicDoc(ws.id, docId);
}

Type guard

function isExpectToRevokePublicDoc(e: unknown): boolean {
  return (
    typeof e === 'object' && e !== null &&
    (e as { extensions?: { code?: string } }).extensions?.code === 'expect_to_revoke_public_doc'
  );
}

Try / catch

try {
  await revokePublicDoc(ws.id, docId);
} catch (e) {
  if (isExpectToRevokePublicDoc(e)) {
    // legacy record pointed at the root; nothing to revoke, treat as success
    return;
  } else throw e;
}

Prevention

When it happens

Trigger: Calling revokePublicDoc(workspaceId, docId) with the workspace id in both arguments — stale id from an older public-doc record, or a client defaulting docId to the workspace root.

Common situations: Migration code replaying public-doc lists that stored the workspace id as docId; UI passing the workspace root id for the 'home' page.

Related errors


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

Appendix: source

Thrown at packages/backend/server/src/core/workspaces/resolvers/doc.ts:463

    this.logger.log(
      `Publish page ${docId} with mode ${mode} in workspace ${workspaceId}`
    );

    return doc;
  }

  @Mutation(() => DocType)
  async revokePublicDoc(
    @CurrentUser() user: CurrentUser,
    @Args('workspaceId') workspaceId: string,
    @Args('docId') docId: string
  ) {
    if (workspaceId === docId) {
      this.logger.error('Expect to revoke public doc, but it is a workspace', {
        workspaceId,
        docId,
      });
      throw new ExpectToRevokePublicDoc('Expect doc not to be workspace');
    }

    await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Publish');

    const doc = await this.models.doc.unpublish(workspaceId, docId);
    this.event.emit('doc.public_state.changed', { workspaceId, docId });

    this.logger.log(`Revoke public doc ${docId} in workspace ${workspaceId}`);

    return doc;
  }

  private async tryFixDocOwner(workspaceId: string, docId: string) {
    const allowed = await this.cache.setnx(
      `fixingOwner:${workspaceId}:${docId}`,
      1,
      // TODO(@forehalo): we definitely need a timer helper
      { ttl: 1000 * 60 * 60 * 24 }

View on GitHub (pinned to b6de0ad51b)