immich-app/immich · warning · BadRequestException
Metadata with key "${key}" not found for asset with id "${id
Error message
Metadata with key "${key}" not found for asset with id "${id}" What it means
Thrown by AssetService.getMetadataByKey when handling GET /assets/:id/metadata/:key. After the access check passes, the service calls assetRepository.getMetadataByKey(id, key); a null/undefined result means no metadata row exists for that key on the asset, so it raises a 400 BadRequest. It indicates the key was never written (or was deleted), not that the asset itself is missing — the asset resolved but the requested key did not.
Source
Thrown at server/src/services/asset.service.ts:433
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);
}
return this.assetRepository.upsertMetadata(id, dto.items);
}
async getMetadataByKey(auth: AuthDto, id: string, key: string): Promise<AssetMetadataResponseDto> {
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
const item = await this.assetRepository.getMetadataByKey(id, key);
if (!item) {
throw new BadRequestException(`Metadata with key "${key}" not found for asset with id "${id}"`);
}
return item;
}
async deleteMetadataByKey(auth: AuthDto, id: string, key: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] });
return this.assetRepository.deleteMetadataByKey(id, key);
}
async deleteBulkMetadata(auth: AuthDto, dto: AssetMetadataBulkDeleteDto) {
await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.items.map((item) => item.assetId) });
await this.assetRepository.deleteBulkMetadata(dto.items);
}
async run(auth: AuthDto, dto: AssetJobsDto) {
await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds });
const jobs: JobItem[] = [];View on GitHub (pinned to 199723261c)
Solutions
- Call GET /assets/{id}/metadata (the list endpoint) first and confirm the key is present before fetching it by key.
- Match the exact key string and casing used when the metadata was upserted via PUT /assets/{id}/metadata.
- If the key is expected to exist, upsert it with PUT /assets/{id}/metadata before reading it back.
- Treat a 400 with this message as a 404-equivalent in the client (e.g. show 'no such metadata') rather than retrying blindly.
Example fix
// before
const meta = await api.get(`/assets/${id}/metadata/${key}`);
// after
const all = await api.get(`/assets/${id}/metadata`);
const exists = all.data.some((m) => m.key === key);
if (!exists) {
return null; // no such metadata
}
const meta = await api.get(`/assets/${id}/metadata/${key}`); Defensive patterns
Strategy: validation
Validate before calling
// Before fetching by key, confirm the key exists.
const { data } = await api.get(`/assets/${id}/metadata`); // returns AssetMetadataResponseDto[]
const keyExists = Array.isArray(data) && data.some((m) => m.key === key);
if (!keyExists) {
return null; // no such metadata — do not call the by-key endpoint
}
const meta = await api.get(`/assets/${id}/metadata/${key}`); Try / catch
try {
return await api.get(`/assets/${id}/metadata/${key}`);
} catch (e) {
if (e.response?.status === 400 && e.response?.data?.message?.includes('not found')) return null;
throw e;
} Prevention
- Always list metadata keys before fetching a specific one.
- Use the exact key string and casing produced by the upsert endpoint.
- Treat this 400 as a not-found in client error mapping.
When it happens
Trigger: Calling GET /assets/{id}/metadata/{key} for a key that was never upserted; using a key whose case differs from how it was stored; calling it after DELETE /assets/{id}/metadata/{key} removed that key; querying before metadata extraction has written any keys.
Common situations: Frontend reading a specific EXIF/user metadata field that was never set for that upload; key name typo; race condition where the client reads metadata immediately after upload before the metadata job populates it; assuming a key exists because it exists on other assets.
Related errors
- Asset not found or asset is not a video
- Both assets must exist
- Asset dimensions are not available for editing
- Library ${id} not found
- Notification not found
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/5d4ddb9797d8dc7f.
Report an issue: GitHub.