immich-app/immich · warning · BadRequestException

Duplicate items are not allowed: "${key}"

Error message

Duplicate items are not allowed: "${key}"

What it means

Thrown by AssetService.upsertBulkMetadata when two items in dto.items produce the same composite key `(assetId, key)`. The key is constructed as the string `(item.assetId, item.key)` and tracked in a Set; a duplicate means the caller sent two metadata values for the same asset+key pair in one batch. BadRequestException (HTTP 400). This is a client-side data error — the server has not written anything yet.

Source

Thrown at server/src/services/asset.service.ts:404

    }

    const dimensions = getDimensions({
      exifImageHeight: asset.exifImageHeight,
      exifImageWidth: asset.exifImageWidth,
      orientation: asset.orientation,
    });

    return ocr.map((item) => transformOcrBoundingBox(item, asset.edits, dimensions));
  }

  async upsertBulkMetadata(auth: AuthDto, dto: AssetMetadataBulkUpsertDto): Promise<AssetMetadataBulkResponseDto[]> {
    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.items.map((item) => item.assetId) });

    const uniqueKeys = new Set<string>();
    for (const item of dto.items) {
      const key = `(${item.assetId}, ${item.key})`;
      if (uniqueKeys.has(key)) {
        throw new BadRequestException(`Duplicate items are not allowed: "${key}"`);
      }

      uniqueKeys.add(key);
    }

    return this.assetRepository.upsertBulkMetadata(dto.items);
  }

  async upsertMetadata(auth: AuthDto, id: string, dto: AssetMetadataUpsertDto): Promise<AssetMetadataResponseDto[]> {
    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] });

    const uniqueKeys = new Set<string>();
    for (const { key } of dto.items) {
      if (uniqueKeys.has(key)) {
        throw new BadRequestException(`Duplicate items are not allowed: "${key}"`);
      }

      uniqueKeys.add(key);

View on GitHub (pinned to 199723261c)

Solutions

  1. Dedupe items by (assetId, key), keeping the desired value, before submitting
  2. Build the items list from a Map keyed by `${assetId}:${key}` so duplicates overwrite
  3. Validate the items array client-side and surface duplicates to the user

Example fix

// before
const items = [...fromExif, ...fromXmp]; // may overlap
await sdk.upsertBulkMetadata({ items });
// after
const map = new Map();
for (const it of [...fromExif, ...fromXmp]) map.set(`(${it.assetId}, ${it.key})`, it);
await sdk.upsertBulkMetadata({ items: [...map.values()] });
Defensive patterns

Strategy: validation

Validate before calling

// Dedupe bulk metadata items by (assetId, key) before submitting
const map = new Map<string, AssetMetadataItem>();
for (const it of items) {
  map.set(`(${it.assetId}, ${it.key})`, it); // last-wins
}
const deduped = [...map.values()];
if (deduped.length !== items.length) {
  console.warn(`Removed ${items.length - deduped.length} duplicate metadata items`);
}
await sdk.upsertBulkMetadata({ items: deduped });

Type guard

function hasNoDuplicateAssetKeys(items: { assetId: string; key: string }[]): boolean {
  const seen = new Set<string>();
  for (const it of items) {
    const k = `(${it.assetId}, ${it.key})`;
    if (seen.has(k)) return false;
    seen.add(k);
  }
  return true;
}

Prevention

When it happens

Trigger: POST /assets/metadata/bulk-upsert (or the bulk metadata endpoint) where dto.items contains two entries with the same assetId AND the same key. E.g. two items both `{ assetId: 'a1', key: 'exif.fNumber', value: ... }`.

Common situations: Concatenating metadata from multiple sources without deduplication; batch built by merging maps where last-wins was intended; client bug appending instead of replacing.

Related errors


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