{"record":{"id":"3c0f71b6b36e74e5","repo":"immich-app/immich","slug":"duplicate-items-are-not-allowed-key","errorCode":null,"errorMessage":"Duplicate items are not allowed: \"${key}\"","messagePattern":"Duplicate items are not allowed: \"(.+?)\"","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/asset.service.ts","lineNumber":404,"sourceCode":"    }\n\n    const dimensions = getDimensions({\n      exifImageHeight: asset.exifImageHeight,\n      exifImageWidth: asset.exifImageWidth,\n      orientation: asset.orientation,\n    });\n\n    return ocr.map((item) => transformOcrBoundingBox(item, asset.edits, dimensions));\n  }\n\n  async upsertBulkMetadata(auth: AuthDto, dto: AssetMetadataBulkUpsertDto): Promise<AssetMetadataBulkResponseDto[]> {\n    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.items.map((item) => item.assetId) });\n\n    const uniqueKeys = new Set<string>();\n    for (const item of dto.items) {\n      const key = `(${item.assetId}, ${item.key})`;\n      if (uniqueKeys.has(key)) {\n        throw new BadRequestException(`Duplicate items are not allowed: \"${key}\"`);\n      }\n\n      uniqueKeys.add(key);\n    }\n\n    return this.assetRepository.upsertBulkMetadata(dto.items);\n  }\n\n  async upsertMetadata(auth: AuthDto, id: string, dto: AssetMetadataUpsertDto): Promise<AssetMetadataResponseDto[]> {\n    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] });\n\n    const uniqueKeys = new Set<string>();\n    for (const { key } of dto.items) {\n      if (uniqueKeys.has(key)) {\n        throw new BadRequestException(`Duplicate items are not allowed: \"${key}\"`);\n      }\n\n      uniqueKeys.add(key);","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/asset.service.ts#L386-L422","documentation":"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.","triggerScenarios":"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: ... }`.","commonSituations":"Concatenating metadata from multiple sources without deduplication; batch built by merging maps where last-wins was intended; client bug appending instead of replacing.","solutions":["Dedupe items by (assetId, key), keeping the desired value, before submitting","Build the items list from a Map keyed by `${assetId}:${key}` so duplicates overwrite","Validate the items array client-side and surface duplicates to the user"],"exampleFix":"// before\nconst items = [...fromExif, ...fromXmp]; // may overlap\nawait sdk.upsertBulkMetadata({ items });\n// after\nconst map = new Map();\nfor (const it of [...fromExif, ...fromXmp]) map.set(`(${it.assetId}, ${it.key})`, it);\nawait sdk.upsertBulkMetadata({ items: [...map.values()] });","handlingStrategy":"validation","validationCode":"// Dedupe bulk metadata items by (assetId, key) before submitting\nconst map = new Map<string, AssetMetadataItem>();\nfor (const it of items) {\n  map.set(`(${it.assetId}, ${it.key})`, it); // last-wins\n}\nconst deduped = [...map.values()];\nif (deduped.length !== items.length) {\n  console.warn(`Removed ${items.length - deduped.length} duplicate metadata items`);\n}\nawait sdk.upsertBulkMetadata({ items: deduped });","typeGuard":"function hasNoDuplicateAssetKeys(items: { assetId: string; key: string }[]): boolean {\n  const seen = new Set<string>();\n  for (const it of items) {\n    const k = `(${it.assetId}, ${it.key})`;\n    if (seen.has(k)) return false;\n    seen.add(k);\n  }\n  return true;\n}","tryCatchPattern":null,"preventionTips":["Build bulk metadata from a Map keyed by `${assetId}:${key}` so duplicates overwrite","Validate the items array with hasNoDuplicateAssetKeys before submitting","Merge metadata sources with last-wins semantics rather than array concat"],"tags":["metadata","validation","duplicate","batch","nestjs"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}