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
- 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.
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
- 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.
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
- Invalid assetId for feature face or asset is offline
- Person not found
- Asset does not have valid dimensions
- Unsupported file type ${filename}
- May not request original file
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/bbdbd8880f6413dd.
Report an issue: GitHub.