immich-app/immich · error · BadRequestException

Cannot leave a cluster group without any other members

Error message

Cannot leave a cluster group without any other members

What it means

Immich's ClusterGroupService.leave (POST /cluster-groups/:id/leave, Permission.ClusterGroupLeave) checks hasOtherMembers before letting a user leave a cluster group. A cluster group must always keep at least one member because it owns the group's shared person/face clustering data; if the leaver is the last member the request is rejected with BadRequestException (HTTP 400). On a successful leave, the server creates a brand-new cluster group for the user, reassigns their people to it (personRepository.reassignCluster), and points the user record at it.

Source

Thrown at server/src/services/cluster-group.service.ts:92

  async regeneratePeople(auth: AuthDto, id: string) {
    await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [id] });

    await this.jobRepository.queue({
      name: JobName.FacialRecognitionQueueAll,
      data: { clusterGroupId: id, force: true },
    });
  }

  async leave(auth: AuthDto, clusterGroupId: string): Promise<void> {
    await this.requireAccess({ auth, permission: Permission.ClusterGroupLeave, ids: [clusterGroupId] });

    const hasOtherMembers = await this.clusterGroupRepository.hasOtherMembers({
      clusterGroupId,
      userId: auth.user.id,
    });
    if (!hasOtherMembers) {
      throw new BadRequestException('Cannot leave a cluster group without any other members');
    }

    const clusterGroup = await this.clusterGroupRepository.create();
    await this.personRepository.reassignCluster({ userId: auth.user.id, newClusterId: clusterGroup.id });
    await this.userRepository.update(auth.user.id, { clusterGroupId: clusterGroup.id });
  }
}

View on GitHub (pinned to 5666d57f15)

Solutions

  1. If you are the last member, don't call leave — dissolve/delete the cluster group through its deletion path instead, or simply stay (a one-member group is equivalent to a personal group).
  2. If the group should survive, have (or invite) another user to join and become the remaining member before you leave.
  3. Guard the call: list the group's users first (GET /cluster-groups/{id}/users) and only call leave when the count is greater than one.

Example fix

// before — last member calls leave blindly
await api.post(`/cluster-groups/${groupId}/leave`);
// => 400 { message: 'Cannot leave a cluster group without any other members' }

// after — check membership before leaving
const users = await api.get(`/cluster-groups/${groupId}/users`);
if (users.length > 1) {
  await api.post(`/cluster-groups/${groupId}/leave`);
} else {
  // last member: delete the group instead of leaving it
}
Defensive patterns

Strategy: validation

Validate before calling

// Before leaving: confirm the group will still have a member.
const users = await api.getClusterGroupUsers(clusterGroupId); // GET /cluster-groups/{id}/users
if (users.length <= 1) {
  throw new Error('Last member cannot leave; delete the cluster group instead');
}
await api.leaveClusterGroup(clusterGroupId); // POST /cluster-groups/{id}/leave

Try / catch

try {
  await api.post(`/cluster-groups/${groupId}/leave`);
} catch (error) {
  if (isHttpError(error, 400, 'Cannot leave a cluster group without any other members')) {
    // terminal state, not retryable: surface 'invite a member or delete the group'
    return notifyUser('You are the last member — delete the group instead of leaving it');
  }
  throw error;
}

Prevention

When it happens

Trigger: POST /cluster-groups/{id}/leave while every other member has already left (or been deleted), so the caller is the group's only remaining member — hasOtherMembers returns false and the 400 is thrown before the new-group creation runs.

Common situations: Members leaving a shared cluster group one by one until the last person tries to leave. Account-deletion or cleanup flows that remove other members first. Integration tests using a single seeded user that call leave directly.

Related errors


AI-assisted analysis of immich-app/immich@5666d57f15 (2026-08-21). Data as JSON: /api/errors/80f05695ac124a9f. Report an issue: GitHub.