immich-app/immich · error · BadRequestException

Live photo video does not belong to the user

Error message

Live photo video does not belong to the user

What it means

Thrown by onBeforeLink when the motion video asset exists and is a video, but its ownerId differs from the userId performing the link. Users may only attach live photo motion videos they own; this enforces per-user asset ownership.

Solutions

  1. Ensure the motion video is uploaded by the same user performing the link
  2. Log in as the owner user or run the operation with the owner's credentials
  3. Copy/upload the motion video under the correct user account before linking
  4. If legitimate sharing is needed, use the official share flow rather than direct linking

Example fix

// before
await onBeforeLink(repos, { userId: me.id, livePhotoVideoId: partnersVideoId });
// after
const motion = await assetRepository.getById(partnersVideoId);
if (motion && motion.ownerId === me.id) {
  await onBeforeLink(repos, { userId: me.id, livePhotoVideoId: partnersVideoId });
} else {
  throw new Error('Motion video must be owned by the linking user');
}
Defensive patterns

Strategy: validation

Validate before calling

if (motionAsset.ownerId !== currentUserId) {
  throw new Error('Motion video must be owned by the linking user');
}

Try / catch

try {
  await linkLivePhoto(assetId, livePhotoVideoId);
} catch (e) {
  if (e.status === 400 && /does not belong/.test(e.message)) {
    // re-upload the video under the current user and retry
  }
}

Prevention

When it happens

Trigger: A user attempts to link a motion video uploaded by another user (e.g. shared asset), or an admin job runs with a mismatched user context.

Common situations: Using a shared album partner's video as your photo's motion component; automation scripts that mix up user credentials; duplicated users after account merge.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/c461e7ff7cb43b1b. Report an issue: GitHub.

Appendix: source

Thrown at server/src/utils/asset.util.ts:157

  return [...partnerIds];
};

export type AssetHookRepositories = { asset: AssetRepository; event: EventRepository };

export const onBeforeLink = async (
  { asset: assetRepository, event: eventRepository }: AssetHookRepositories,
  { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
) => {
  const motionAsset = await assetRepository.getById(livePhotoVideoId);
  if (!motionAsset) {
    throw new BadRequestException('Live photo video not found');
  }
  if (motionAsset.type !== AssetType.Video) {
    throw new BadRequestException('Live photo video must be a video');
  }
  if (motionAsset.ownerId !== userId) {
    throw new BadRequestException('Live photo video does not belong to the user');
  }

  if (motionAsset && motionAsset.visibility === AssetVisibility.Timeline) {
    await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.Hidden });
    await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });
  }
};

export const onBeforeUnlink = async (
  { asset: assetRepository }: AssetHookRepositories,
  { livePhotoVideoId }: { livePhotoVideoId: string },
) => {
  const motion = await assetRepository.getById(livePhotoVideoId);
  if (!motion) {
    return null;
  }

  if (StorageCore.isAndroidMotionPath(motion.originalPath)) {

View on GitHub (pinned to e55ac299a4)