immich-app/immich · warning · BadRequestException

Cannot merge a person into themselves

Error message

Cannot merge a person into themselves

What it means

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.

Source

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

    return JobStatus.Success;
  }

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

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

    return JobStatus.Success;
  }

  async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
    const mergeIds = dto.ids;
    if (mergeIds.includes(id)) {
      throw new BadRequestException('Cannot merge a person into themselves');
    }

    await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] });
    let primaryPerson = await this.findOrFail(id);
    const primaryName = primaryPerson.name || primaryPerson.id;

    const results: BulkIdResponseDto[] = [];

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

    for (const mergeId of mergeIds) {
      const hasAccess = allowedIds.has(mergeId);
      if (!hasAccess) {
        results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION });

View on GitHub (pinned to 199723261c)

Solutions

  1. Filter the destination id out of dto.ids on the client before submitting.
  2. In the UI, disable the primary person in the merge picker.
  3. Treat 400 'Cannot merge a person into themselves' as a client bug and log the offending payload.

Example fix

// before
if (mergeIds.includes(id)) {
  throw new BadRequestException('Cannot merge a person into themselves');
}

// after (silently skip the no-op id instead of failing the whole request)
const uniqueMergeIds = mergeIds.filter((mergeId) => mergeId !== id);
if (uniqueMergeIds.length === 0) {
  return [];
}
Defensive patterns

Strategy: validation

Validate before calling

// Strip the destination id from the merge list before submitting.
const safeIds = dto.ids.filter((mergeId) => mergeId !== id);
if (safeIds.length === 0) return []; // nothing to merge
await personService.mergePerson(auth, id, { ids: safeIds });

Type guard

const isSelfMerge = (id: string, ids: string[]): boolean => ids.includes(id);

Try / catch

try {
  await personService.mergePerson(auth, id, dto);
} catch (e) {
  if (e instanceof BadRequestException && /into themselves/i.test(e.message)) {
    // retry with the self-id filtered out
    return personService.mergePerson(auth, id, { ids: dto.ids.filter((x) => x !== id) });
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/bbdbd8880f6413dd. Report an issue: GitHub.