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
- 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).
- If the group should survive, have (or invite) another user to join and become the remaining member before you leave.
- 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
- Check the group's member list before showing a 'Leave group' action; hide it for single-member groups.
- In multi-user leave flows, process leaves in an order that never strands the final member, or make the last member delete the group.
- Treat this 400 as expected control flow in UIs (show guidance), not as an alert-worthy failure.
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
- Cannot request to join your own cluster group
- Sidecar files cannot be deleted
- Cannot rotate an API Key with permissions you do not have
- The ${name} extension is not available in this Postgres inst
- The ${name} extension version is ${version}, which mean
AI-assisted analysis of immich-app/immich@5666d57f15 (2026-08-21).
Data as JSON: /api/errors/80f05695ac124a9f.
Report an issue: GitHub.