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 does not match the authenticated userId. This enforces ownership so a user cannot link another user's asset as their Live Photo motion component. HTTP 400 BadRequestException.

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 199723261c)

Solutions

  1. Use only motion video asset ids owned by the calling user.
  2. If the motion asset legitimately belongs to a partner, have the owner upload/own it or transfer ownership first.
  3. Check ownership via the access control / GET /assets/:id before linking.

Example fix

// before
await updateAsset({ id: myAsset.id, livePhotoVideoId: partnerAsset.id });

// after
const motion = await getAsset(motionId);
if (motion.ownerId !== currentUser.id) throw new Error('not owner');
await updateAsset({ id: myAsset.id, livePhotoVideoId: motion.id });
Defensive patterns

Strategy: validation

Validate before calling

const motion = await assetRepository.getById(livePhotoVideoId);
const ok = !!motion && motion.ownerId === auth.user.id;

Type guard

const isOwnedBy = (a: { ownerId: string } | null, userId: string): a is { ownerId: string } =>
  !!a && a.ownerId === userId;

Try / catch

try { await updateAsset({ id, livePhotoVideoId }); }
catch (e) { if (/does not belong to the user/.test(e.message)) { notify('Pick one of your own motion assets'); } else throw e; }

Prevention

When it happens

Trigger: Authenticated user A calls AssetService.update with livePhotoVideoId owned by user B; shared-partner asset id mistakenly used as a personal motion asset; cross-account import where ids collide.

Common situations: Using an asset id observed in a shared album or partner's library; id copied from another user's instance; multi-tenant confusion where the same numeric/uuid is reused.

Related errors


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