{"record":{"id":"bbdbd8880f6413dd","repo":"immich-app/immich","slug":"cannot-merge-a-person-into-themselves","errorCode":null,"errorMessage":"Cannot merge a person into themselves","messagePattern":"Cannot merge a person into themselves","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/person.service.ts","lineNumber":544,"sourceCode":"    return JobStatus.Success;\n  }\n\n  @OnJob({ name: JobName.PersonFileMigration, queue: QueueName.Migration })\n  async handlePersonMigration({ id }: JobOf<JobName.PersonFileMigration>): Promise<JobStatus> {\n    const person = await this.personRepository.getById(id);\n    if (!person) {\n      return JobStatus.Failed;\n    }\n\n    await this.storageCore.movePersonFile(person, PersonPathType.Face);\n\n    return JobStatus.Success;\n  }\n\n  async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {\n    const mergeIds = dto.ids;\n    if (mergeIds.includes(id)) {\n      throw new BadRequestException('Cannot merge a person into themselves');\n    }\n\n    await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] });\n    let primaryPerson = await this.findOrFail(id);\n    const primaryName = primaryPerson.name || primaryPerson.id;\n\n    const results: BulkIdResponseDto[] = [];\n\n    const allowedIds = await this.checkAccess({\n      auth,\n      permission: Permission.PersonMerge,\n      ids: mergeIds,\n    });\n\n    for (const mergeId of mergeIds) {\n      const hasAccess = allowedIds.has(mergeId);\n      if (!hasAccess) {\n        results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });","sourceCodeStart":526,"sourceCodeEnd":562,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/person.service.ts#L526-L562","documentation":"Thrown by PersonService.mergePerson when the target id is included in dto.ids. Merging a person into itself is a logical no-op that would corrupt identity, so it is rejected up-front before any access checks. BadRequestException -> HTTP 400.","triggerScenarios":"POST /people/{id}/merge with dto.ids containing the same id as the path parameter; client accidentally echoing the primary person into the merge list.","commonSituations":"UI 'select all' includes the primary person; batch merge built from a full people list without filtering out the destination; copy-paste of person ids.","solutions":["Filter the destination id out of dto.ids on the client before submitting.","In the UI, disable the primary person in the merge picker.","Treat 400 'Cannot merge a person into themselves' as a client bug and log the offending payload."],"exampleFix":"// before\nif (mergeIds.includes(id)) {\n  throw new BadRequestException('Cannot merge a person into themselves');\n}\n\n// after (silently skip the no-op id instead of failing the whole request)\nconst uniqueMergeIds = mergeIds.filter((mergeId) => mergeId !== id);\nif (uniqueMergeIds.length === 0) {\n  return [];\n}","handlingStrategy":"validation","validationCode":"// Strip the destination id from the merge list before submitting.\nconst safeIds = dto.ids.filter((mergeId) => mergeId !== id);\nif (safeIds.length === 0) return []; // nothing to merge\nawait personService.mergePerson(auth, id, { ids: safeIds });","typeGuard":"const isSelfMerge = (id: string, ids: string[]): boolean => ids.includes(id);","tryCatchPattern":"try {\n  await personService.mergePerson(auth, id, dto);\n} catch (e) {\n  if (e instanceof BadRequestException && /into themselves/i.test(e.message)) {\n    // retry with the self-id filtered out\n    return personService.mergePerson(auth, id, { ids: dto.ids.filter((x) => x !== id) });\n  }\n  throw e;\n}","preventionTips":["Filter the destination id out of the merge list client-side.","Disable the primary person row in the merge picker UI.","Treat this 400 as a client bug, not a server error."],"tags":["person","merge","facial-recognition","validation","nestjs"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}