toeverything/AFFiNE · error · NotFoundException

Workspace not found

Error message

Workspace not found

What it means

NotFoundException('Workspace not found') from CopilotResolver.assertPermission: the helper is invoked with options.workspaceId undefined/null (the copilot parent object carries no workspace context), so there is nothing to permission-check. It is a 404 used to avoid leaking whether any workspace exists — distinct from a permission denial, which would come from the ac.assert chain.

Source

Thrown at packages/backend/server/src/plugins/copilot/resolver.ts:413

  }

  @ResolveField(() => CopilotQuotaType, {
    name: 'quota',
    description: 'Get the quota of the user in the workspace',
    complexity: 2,
  })
  async getQuota(@CurrentUser() user: CurrentUser): Promise<CopilotQuotaType> {
    return await this.chatSession.getQuota(user.id);
  }

  private async assertPermission(
    user: CurrentUser,
    options: { workspaceId?: string | null; docId?: string | null },
    fallbackAction?: DocAction
  ) {
    const { workspaceId, docId } = options;
    if (!workspaceId) {
      throw new NotFoundException('Workspace not found');
    }
    if (docId) {
      await this.ac
        .user(user.id)
        .doc({ workspaceId, docId })
        .allowLocal()
        .assert(fallbackAction ?? 'Doc.Update');
    } else {
      await this.ac
        .user(user.id)
        .workspace(workspaceId)
        .allowLocal()
        .assert('Workspace.Copilot');
    }
    return { userId: user.id, workspaceId, docId: docId || undefined };
  }

  @ResolveField(() => CopilotSessionType, {

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Ensure the query provides a copilot parent bound to a server workspaceId (fetch the copilot/workspace object first)
  2. Skip the server-side session queries for local-workspace contexts in the client
  3. If maintaining the API: consider making the missing-workspace case explicit in the schema rather than a 404

Example fix

# before
query { copilot { session(sessionId: "...") { id } } } # copilot.workspaceId null -> 404

# after
query {
  workspace(id: $wsId) {
    copilot { session(sessionId: "...") { id } }
  }
}
Defensive patterns

Strategy: validation

Validate before calling

if (!copilot.workspaceId) {
  // no server workspace context; skip the query or resolve one first
  throw new Error('This view requires a cloud workspace');
}
await graphql(copilotSessionQuery, { workspaceId: copilot.workspaceId });

Type guard

const hasWorkspaceContext = (
  c: { workspaceId?: string | null } | null | undefined
): c is { workspaceId: string } => typeof c?.workspaceId === 'string' && c.workspaceId.length > 0;

Try / catch

try {
  const session = await fetchCopilotSession(copilot, sessionId);
} catch (e) {
  if (e instanceof NotFoundException && /Workspace not found/.test(e.message)) {
    redirectToListOrEnableCloudSync();
  } else throw e;
}

Prevention

When it happens

Trigger: Querying a copilot session/chats field on a CopilotType parent whose workspaceId is null (e.g. a local or account-scoped context); calling resolvers like session()/sessions() without a workspace-bound copilot parent; GraphQL query built with missing workspace context.

Common situations: Client builds the copilot query for local workspaces where no server workspaceId exists; stale query shape after API refactor added workspaceId requirement; null propagating from an upstream resolver.

Related errors


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