toeverything/AFFiNE · error · NotFound

not_found

not_found

Error message

Invitation not found

What it means

Thrown by `WorkspaceService.getInviteInfo` when neither a cached link invite (`workspace:inviteLinkId:${inviteId}`) nor a `workspace_user` row by that id exists. The invitation id is unresolvable. Coded under the generic `not_found` (resource_not_found) category with the literal message 'Invitation not found'.

Source

Thrown at packages/backend/server/src/core/workspaces/service.ts:56

    private readonly runtime: BackendRuntimeProvider
  ) {}

  async getInviteInfo(inviteId: string): Promise<InviteInfo> {
    // invite link
    const invite = await this.cache.get<InviteInfo>(
      `workspace:inviteLinkId:${inviteId}`
    );
    if (typeof invite?.workspaceId === 'string') {
      return {
        ...invite,
        isLink: true,
      };
    }

    const workspaceUser = await this.models.workspaceUser.getById(inviteId);

    if (!workspaceUser) {
      throw new NotFound('Invitation not found');
    }

    return {
      isLink: false,
      workspaceId: workspaceUser.workspaceId,
      inviteeUserId: workspaceUser.userId,
      inviterUserId: workspaceUser.inviterId,
    };
  }

  async getWorkspaceInfo(workspaceId: string) {
    const workspaceContent = await this.doc.getWorkspaceContent(workspaceId);

    let avatar = DEFAULT_WORKSPACE_AVATAR;
    if (workspaceContent?.avatarKey) {
      const avatarBlob = await this.blobStorage.get(
        workspaceId,
        workspaceContent.avatarKey

View on GitHub (pinned to 26c515e050)

Solutions

  1. Request a fresh invitation from the workspace owner/admin.
  2. Verify the invite id is complete and unmodified (URL-decode it first if from a URL).
  3. If you operate the cache, confirm Redis is reachable and the invite-link TTL is sane.
  4. Differentiate link ids from email-invite ids in the client to avoid cross-use.

Example fix

// before
const info = await workspaceService.getInviteInfo(inviteId);

// after
try {
  const info = await workspaceService.getInviteInfo(inviteId);
} catch (e) {
  if (e.code === 'not_found') {
    throw new ClientVisibleError('This invitation no longer exists. Please ask for a new one.');
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Best-effort pre-check via the resolver-level getInviteInfo
const info = await sdk.getInviteInfo({ inviteId }).catch(() => null);
if (!info) {
  throw new ClientVisibleError('This invitation no longer exists.');
}

Type guard

function isResolvableInvite(invite) {
  return Boolean(invite && (invite.isLink || invite.workspaceId));
}

Try / catch

try {
  const info = await workspaceService.getInviteInfo(inviteId);
} catch (e) {
  if (e.code === 'not_found') {
    throw new ClientVisibleError('Invitation invalid or expired. Request a new one.');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `getInviteInfo(inviteId)` (directly or via the resolver) with an id that is not a link invite in the cache and not a `workspace_user` id. Typoed id, expired/evicted link, revoked email invite, or an id from a different environment.

Common situations: Old link invite whose Redis entry TTL'd; admin deleted the pending invitation row; id was truncated when copied; link from staging used against production; the invitation was already accepted and the row mutated.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/d1a4aba2326a2cf5. Report an issue: GitHub.