{"record":{"id":"80f05695ac124a9f","repo":"immich-app/immich","slug":"cannot-leave-a-cluster-group-without-any-other-mem","errorCode":null,"errorMessage":"Cannot leave a cluster group without any other members","messagePattern":"Cannot leave a cluster group without any other members","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/cluster-group.service.ts","lineNumber":92,"sourceCode":"\n  async regeneratePeople(auth: AuthDto, id: string) {\n    await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [id] });\n\n    await this.jobRepository.queue({\n      name: JobName.FacialRecognitionQueueAll,\n      data: { clusterGroupId: id, force: true },\n    });\n  }\n\n  async leave(auth: AuthDto, clusterGroupId: string): Promise<void> {\n    await this.requireAccess({ auth, permission: Permission.ClusterGroupLeave, ids: [clusterGroupId] });\n\n    const hasOtherMembers = await this.clusterGroupRepository.hasOtherMembers({\n      clusterGroupId,\n      userId: auth.user.id,\n    });\n    if (!hasOtherMembers) {\n      throw new BadRequestException('Cannot leave a cluster group without any other members');\n    }\n\n    const clusterGroup = await this.clusterGroupRepository.create();\n    await this.personRepository.reassignCluster({ userId: auth.user.id, newClusterId: clusterGroup.id });\n    await this.userRepository.update(auth.user.id, { clusterGroupId: clusterGroup.id });\n  }\n}\n","sourceCodeStart":74,"sourceCodeEnd":100,"githubUrl":"https://github.com/immich-app/immich/blob/5666d57f15a66bd5518119c5d9f4d2b62f3a86c1/server/src/services/cluster-group.service.ts#L74-L100","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — last member calls leave blindly\nawait api.post(`/cluster-groups/${groupId}/leave`);\n// => 400 { message: 'Cannot leave a cluster group without any other members' }\n\n// after — check membership before leaving\nconst users = await api.get(`/cluster-groups/${groupId}/users`);\nif (users.length > 1) {\n  await api.post(`/cluster-groups/${groupId}/leave`);\n} else {\n  // last member: delete the group instead of leaving it\n}","handlingStrategy":"validation","validationCode":"// Before leaving: confirm the group will still have a member.\nconst users = await api.getClusterGroupUsers(clusterGroupId); // GET /cluster-groups/{id}/users\nif (users.length <= 1) {\n  throw new Error('Last member cannot leave; delete the cluster group instead');\n}\nawait api.leaveClusterGroup(clusterGroupId); // POST /cluster-groups/{id}/leave","typeGuard":null,"tryCatchPattern":"try {\n  await api.post(`/cluster-groups/${groupId}/leave`);\n} catch (error) {\n  if (isHttpError(error, 400, 'Cannot leave a cluster group without any other members')) {\n    // terminal state, not retryable: surface 'invite a member or delete the group'\n    return notifyUser('You are the last member — delete the group instead of leaving it');\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["immich","cluster-group","membership","business-rule","http-400","group-management"],"backgroundTag":"leave-group-last-member","analyzedSha":"5666d57f15a66bd5518119c5d9f4d2b62f3a86c1","analyzedAt":"2026-08-21T18:08:19.313Z","contentChangedAt":"2026-08-21T18:08:19.313Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}