{"record":{"id":"f2c5bd8cda0310f9","repo":"immich-app/immich","slug":"both-assets-must-exist","errorCode":null,"errorMessage":"Both assets must exist","messagePattern":"Both assets must exist","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/asset.service.ts","lineNumber":205,"sourceCode":"\n  async copy(\n    auth: AuthDto,\n    {\n      sourceId,\n      targetId,\n      albums = true,\n      sidecar = true,\n      sharedLinks = true,\n      stack = true,\n      favorite = true,\n    }: AssetCopyDto,\n  ) {\n    await this.requireAccess({ auth, permission: Permission.AssetCopy, ids: [sourceId, targetId] });\n    const sourceAsset = await this.assetRepository.getForCopy(sourceId);\n    const targetAsset = await this.assetRepository.getForCopy(targetId);\n\n    if (!sourceAsset || !targetAsset) {\n      throw new BadRequestException('Both assets must exist');\n    }\n\n    if (sourceId === targetId) {\n      throw new BadRequestException('Source and target id must be distinct');\n    }\n\n    if (albums) {\n      await this.albumRepository.copyAlbums({ sourceAssetId: sourceId, targetAssetId: targetId });\n    }\n\n    if (sharedLinks) {\n      await this.sharedLinkAssetRepository.copySharedLinks({ sourceAssetId: sourceId, targetAssetId: targetId });\n    }\n\n    if (stack) {\n      await this.copyStack({ sourceAsset, targetAsset });\n    }\n","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/services/asset.service.ts#L187-L223","documentation":"copy duplicates metadata (albums, tags, etc.) from a source asset to a target asset. It fetches both via getForCopy and requires both to resolve; if either id is missing or inaccessible it throws BadRequestException('Both assets must exist') before any copying happens.","triggerScenarios":"POST /api/assets/copy (or the copy mutation) where sourceId or targetId does not exist, was deleted, or is not visible to the authenticated user / shared link.","commonSituations":"Client UIs allowing copy between albums after one asset was trashed; scripts passing placeholder or cross-instance ids; duplicated-asset workflows racing with trash emptying.","solutions":["Resolve both ids first (GET /api/assets/:id) and confirm both return 200 before copying.","Re-select the target asset from a fresh asset list if the old id is stale.","Ensure both assets belong to the same Immich instance and are visible to the current auth context.","Handle the error by surfacing 'source or target no longer exists' to the user and aborting the copy."],"exampleFix":"// before\nawait api.copyAssets(sourceId, maybeDeletedTargetId);\n// after\nconst [s, t] = await Promise.all([api.getAssetInfo(sourceId), api.getAssetInfo(targetId)].map((p) => p.catch(() => null)));\nif (s && t) await api.copyAssets(sourceId, targetId);\nelse throw new Error('Source or target asset no longer exists');","handlingStrategy":"validation","validationCode":"const [s, t] = await Promise.all([api.getAssetInfo(sourceId), api.getAssetInfo(targetId)].map((p) => p.catch(() => null)));\nif (!s || !t) throw new Error('Both source and target must exist before copy');","typeGuard":null,"tryCatchPattern":"try { await api.copyAssets(sourceId, targetId); } catch (e) { if (String(e).includes('Both assets must exist')) abortCopyAndRefresh(); else throw e; }","preventionTips":["Resolve both ids immediately before copying; never use long-lived ids","Ensure source and target come from the same instance/auth context","Re-select targets from fresh album listings"],"tags":["not-found","copy","asset","precondition"],"backgroundTag":"entity-not-found","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}