immich-app/immich · error · BadRequestException

Both assets must exist

Error message

Both assets must exist

What it means

Thrown by AssetService.copyAssetMetadata when either getForCopy(sourceId) or getForCopy(targetId) returns null. requireAccess for AssetCopy on both ids already passed, so this typically means one or both assets were deleted between the access check and the fetch, or the ids are wrong. BadRequestException (HTTP 400).

Source

Thrown at server/src/services/asset.service.ts:200

  async copy(
    auth: AuthDto,
    {
      sourceId,
      targetId,
      albums = true,
      sidecar = true,
      sharedLinks = true,
      stack = true,
      favorite = true,
    }: AssetCopyDto,
  ) {
    await this.requireAccess({ auth, permission: Permission.AssetCopy, ids: [sourceId, targetId] });
    const sourceAsset = await this.assetRepository.getForCopy(sourceId);
    const targetAsset = await this.assetRepository.getForCopy(targetId);

    if (!sourceAsset || !targetAsset) {
      throw new BadRequestException('Both assets must exist');
    }

    if (sourceId === targetId) {
      throw new BadRequestException('Source and target id must be distinct');
    }

    if (albums) {
      await this.albumRepository.copyAlbums({ sourceAssetId: sourceId, targetAssetId: targetId });
    }

    if (sharedLinks) {
      await this.sharedLinkAssetRepository.copySharedLinks({ sourceAssetId: sourceId, targetAssetId: targetId });
    }

    if (stack) {
      await this.copyStack({ sourceAsset, targetAsset });
    }

View on GitHub (pinned to 199723261c)

Solutions

  1. Confirm both source and target assets exist (GET /assets) before calling copy
  2. Filter out ids missing from the current asset list before a bulk copy
  3. Handle 400 by skipping the pair and continuing the batch

Example fix

// before
await sdk.copyAsset(sourceId, targetId, opts);
// after
const [src, tgt] = await Promise.all([sdk.getAsset(sourceId), sdk.getAsset(targetId)]);
if (!src || !tgt) throw new Error('source or target missing');
await sdk.copyAsset(sourceId, targetId, opts);
Defensive patterns

Strategy: validation

Validate before calling

// Confirm both source and target exist before copying
const [src, tgt] = await Promise.all([sdk.getAsset(sourceId), sdk.getAsset(targetId)]);
if (!src || !tgt) {
  throw new Error('source or target asset is missing');
}
await sdk.copyAsset(sourceId, targetId, opts);

Type guard

function bothExist<T>(a: T | null, b: T | null): a is T {
  return a !== null && b !== null;
}

Prevention

When it happens

Trigger: POST /assets/copy (or copy endpoint) supplying a sourceId or targetId that does not exist, or that is deleted concurrently with the copy call.

Common situations: Bulk copy where one asset in the batch was removed; cross-album copy with a stale id; client constructing target ids incorrectly.

Related errors


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