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

  1. Call GET /assets/{id}/metadata (the list endpoint) first and confirm the key is present before fetching it by key.
  2. Match the exact key string and casing used when the metadata was upserted via PUT /assets/{id}/metadata.
  3. If the key is expected to exist, upsert it with PUT /assets/{id}/metadata before reading it back.
  4. 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

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


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