immich-app/immich · error · BadRequestException
Duplicate items are not allowed
Error message
Duplicate items are not allowed: "${key}" What it means
upsertBulkMetadata rejects a bulk metadata upsert when two items share the same (assetId, key) pair, since the upsert would write the same metadata key twice for one asset. The server builds the composite key string and throws BadRequestException before touching the database. Each asset/key pair must appear at most once in dto.items.
Solutions
- Deduplicate dto.items by (assetId, key) before sending the request
- If multiple values are intended, use distinct keys (e.g. key-1, key-2) or the dedicated bulk metadata endpoints for multiple values
- Log and drop later duplicates client-side when building the payload
- Ensure sync jobs replace rather than append metadata items
Example fix
// before
const items = [...oldItems, ...newItems];
await api.upsertBulkMetadata({ items });
// after
const seen = new Set<string>();
const items = [...oldItems, ...newItems].filter((i) => {
const k = `${i.assetId}:${i.key}`;
if (seen.has(k)) return false;
seen.add(k);
return true;
});
await api.upsertBulkMetadata({ items }); Defensive patterns
Strategy: validation
Validate before calling
const seen = new Set<string>();
for (const i of items) {
const k = `${i.assetId}:${i.key}`;
if (seen.has(k)) throw new Error(`Duplicate metadata item: ${k}`);
seen.add(k);
} Try / catch
try { await api.upsertBulkMetadata({ items }); } catch (e) { if (String(e?.response?.data?.message).includes('Duplicate items')) { dedupeItems(); retryOnce(); } else throw e; } Prevention
- Always dedupe by (assetId, key) when merging metadata sources
- Build items with a Map keyed on the composite key
- Avoid append-style sync jobs for metadata
- Unit-test payload builders for duplicate keys
When it happens
Trigger: PUT /api/assets/metadata (bulk) with dto.items containing two entries where both assetId and key are identical, e.g. [{assetId:'a',key:'face-1'},{assetId:'a',key:'face-1'}].
Common situations: Merging metadata lists from multiple sources (e.g. duplicate face entries in EXIF/ML results); concatenating per-asset arrays without deduplication; re-running a sync job that appends instead of replacing items.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Asset dimensions are not available for editing
- Cannot merge a person into themselves
- Metadata with key " " not found for asset with id
- A tag with that name already exists
- Asset does not have valid dimensions
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/3c0f71b6b36e74e5.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/asset.service.ts:409
}
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 e55ac299a4)