immich-app/immich · warning · BadRequestException

Cannot merge a person into themselves

Error message

Cannot merge a person into themselves

What it means

mergePeople rejects an ids array containing duplicates, since merging a person into itself is a no-op and would corrupt the bulk-id response contract. It compares Set size to array length and throws BadRequestException('Cannot merge a person into themselves') when they differ.

Solutions

  1. De-duplicate the ids array before calling: [...new Set(ids)].
  2. Exclude the primary person's ID from the ids payload; the URL already identifies the primary.
  3. Fix selection UI to prevent picking the same person twice.
  4. If you intended to change the primary person, use the person update endpoint instead of merge.

Example fix

// before
await api.peopleApi.mergePerson({ id: primaryId, mergePersonDto: { ids: [dupA, dupA, primaryId] } });
// after
const ids = [...new Set([dupA, dupA, primaryId])].filter((id) => id !== primaryId);
await api.peopleApi.mergePerson({ id: primaryId, mergePersonDto: { ids } });
Defensive patterns

Strategy: validation

Validate before calling

const deduped = [...new Set(ids)];
if (deduped.length !== ids.length) throw new Error('Duplicate person IDs in merge request');

Type guard

function hasNoDuplicates(ids) {
  return Array.isArray(ids) && new Set(ids).size === ids.length;
}

Try / catch

try {
  await api.peopleApi.mergePerson({ id: primaryId, mergePersonDto: { ids } });
} catch (e) {
  if (e.status === 400 && String(e.message).includes('merge a person into themselves')) {
    ids = [...new Set(ids)].filter((id) => id !== primaryId); // retry once deduped
  } else throw e;
}

Prevention

When it happens

Trigger: POST merge with duplicate IDs in the ids array — e.g. ids: ["a", "a"], or the primary person's own ID repeated in the body list.

Common situations: Client selection logic adding the same person twice (double-click, not de-duplicating), or scripts merging a person into itself thinking it renames/keeps the primary.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/bbdbd8880f6413dd. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/person.service.ts:593

  @OnJob({ name: JobName.PersonFileMigration, queue: QueueName.Migration })
  async handlePersonMigration({ ownerId, personGroupId }: JobOf<JobName.PersonFileMigration>): Promise<JobStatus> {
    const person = await this.personRepository.getByGroupId({ ownerId, personGroupId });
    if (!person) {
      return JobStatus.Failed;
    }

    await this.storageCore.movePersonFile(person, PersonPathType.Face);

    return JobStatus.Success;
  }

  async mergePeople(auth: AuthDto, { ids }: MergePersonDto): Promise<BulkIdResponseDto[]> {
    if (ids.length < 2) {
      throw new BadRequestException('At least two people are required for merging');
    }

    if (new Set(ids).size !== ids.length) {
      throw new BadRequestException('Cannot merge a person into themselves');
    }

    const results: BulkIdResponseDto[] = [];

    const allowedIds = await this.checkAccess({ auth, permission: Permission.PersonMerge, ids });

    const peopleMap: Record<string, Selectable<PersonTable>[]> = {};

    for (const mergePerson of await this.personRepository.getForMergePerson(ids)) {
      if (!peopleMap[mergePerson.personGroupId]) {
        peopleMap[mergePerson.personGroupId] = [];
      }
      peopleMap[mergePerson.personGroupId].push(mergePerson);
    }

    const targetPeople: Record<string, Selectable<PersonTable>> = {};
    for (const mergeId of ids) {
      const hasAccess = allowedIds.has(mergeId);

View on GitHub (pinned to f48d4b3321)