{"record":{"id":"5d4ddb9797d8dc7f","repo":"immich-app/immich","slug":"metadata-with-key-key-not-found-for-asset-wit","errorCode":null,"errorMessage":"Metadata with key \"${key}\" not found for asset with id \"${id}\"","messagePattern":"Metadata with key \"(.+?)\" not found for asset with id \"(.+?)\"","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/asset.service.ts","lineNumber":433,"sourceCode":"\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);\n    }\n\n    return this.assetRepository.upsertMetadata(id, dto.items);\n  }\n\n  async getMetadataByKey(auth: AuthDto, id: string, key: string): Promise<AssetMetadataResponseDto> {\n    await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });\n\n    const item = await this.assetRepository.getMetadataByKey(id, key);\n    if (!item) {\n      throw new BadRequestException(`Metadata with key \"${key}\" not found for asset with id \"${id}\"`);\n    }\n    return item;\n  }\n\n  async deleteMetadataByKey(auth: AuthDto, id: string, key: string): Promise<void> {\n    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] });\n    return this.assetRepository.deleteMetadataByKey(id, key);\n  }\n\n  async deleteBulkMetadata(auth: AuthDto, dto: AssetMetadataBulkDeleteDto) {\n    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.items.map((item) => item.assetId) });\n    await this.assetRepository.deleteBulkMetadata(dto.items);\n  }\n\n  async run(auth: AuthDto, dto: AssetJobsDto) {\n    await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds });\n\n    const jobs: JobItem[] = [];","sourceCodeStart":415,"sourceCodeEnd":451,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/asset.service.ts#L415-L451","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst meta = await api.get(`/assets/${id}/metadata/${key}`);\n\n// after\nconst all = await api.get(`/assets/${id}/metadata`);\nconst exists = all.data.some((m) => m.key === key);\nif (!exists) {\n  return null; // no such metadata\n}\nconst meta = await api.get(`/assets/${id}/metadata/${key}`);","handlingStrategy":"validation","validationCode":"// Before fetching by key, confirm the key exists.\nconst { data } = await api.get(`/assets/${id}/metadata`); // returns AssetMetadataResponseDto[]\nconst keyExists = Array.isArray(data) && data.some((m) => m.key === key);\nif (!keyExists) {\n  return null; // no such metadata — do not call the by-key endpoint\n}\nconst meta = await api.get(`/assets/${id}/metadata/${key}`);","typeGuard":null,"tryCatchPattern":"try {\n  return await api.get(`/assets/${id}/metadata/${key}`);\n} catch (e) {\n  if (e.response?.status === 400 && e.response?.data?.message?.includes('not found')) return null;\n  throw e;\n}","preventionTips":["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."],"tags":["asset","metadata","api","not-found","validation","rest"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}