toeverything/AFFiNE · error · MemberNotFoundInSpace
member_not_found_in_space
member_not_found_in_space
Error message
Member not found in Space ${spaceId}. What it means
Thrown by the approveMember mutation when models.workspaceUser.get(workspaceId, userId) returns no row — the user being approved has no membership record in the workspace. It fires when the role lookup fails entirely, as opposed to a member who exists but is not UnderReview (which returns true silently). Code member_not_found_in_space, type action_forbidden, HTTP 403, data.spaceId = workspaceId.
Solutions
- Refresh the pending-members list and confirm the user still has a membership row in this workspace before approving.
- If the row is gone, treat the request as already handled/cancelled — re-sync UI state instead of retrying.
- Double-check that userId belongs to the same workspaceId (copy-paste errors across workspaces are common).
Example fix
// before
await approveMember(ws.id, userId); // row already removed -> member_not_found_in_space
// after
const member = await getWorkspaceMember(ws.id, userId);
if (!member || member.status !== 'UnderReview') {
await refetchPendingList(); // someone else handled it
return;
}
await approveMember(ws.id, userId); Defensive patterns
Strategy: try-catch
Validate before calling
// confirm the membership row is still pending before approving
const member = await getWorkspaceMember(workspaceId, userId);
if (!member || member.status !== 'UnderReview') {
await refetchPendingList(); // handled elsewhere or withdrawn
} Type guard
function isMemberNotFoundInSpace(e: unknown): boolean {
const ext = (e as { extensions?: Record<string, unknown> })?.extensions;
return String(ext?.name ?? '').toLowerCase() === 'member_not_found_in_space';
} Try / catch
try {
await approveMember(workspaceId, userId);
} catch (e) {
if (isMemberNotFoundInSpace(e)) {
await refetchPendingList(); // request already handled — sync UI, no retry
} else throw e;
} Prevention
- Refetch the pending-review list when the view gains focus.
- Coordinate approvals across admins (or lock rows) to avoid double-processing.
When it happens
Trigger: Calling approveMember with a userId that was never a member, was already removed/revoked, or whose join request was withdrawn — e.g. two admins acting on the same request and one already handled or rejected it.
Common situations: Stale 'pending review' list in the admin UI after another admin processed the request; approving a user removed by an owner concurrently; passing a wrong userId (typo or from another workspace).
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/49ea4352a4569018.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:438
} catch (error) {
if (backendRuntimeErrorCode(error) === 'seat_limit') {
throw new NoMoreSeat({ spaceId: workspaceId });
}
throw error;
}
this.event.emit('workspace.members.updated', {
workspaceId,
});
await this.workspaceService.sendReviewApprovedNotification(
role.id,
me.id
);
}
return true;
} else {
throw new MemberNotFoundInSpace({ spaceId: workspaceId });
}
}
@Mutation(() => Boolean)
async grantMember(
@CurrentUser() user: CurrentUser,
@Args('workspaceId') workspaceId: string,
@Args('userId') userId: string,
@Args('permission', { type: () => WorkspaceRole }) newRole: WorkspaceRole
) {
let role: 'member' | 'admin' | 'owner';
switch (newRole) {
case WorkspaceRole.External:
return this.revokeMember(user, workspaceId, userId);
case WorkspaceRole.Collaborator:
role = 'member';
break;
case WorkspaceRole.Admin:View on GitHub (pinned to 2af30773ae)