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
- Use only motion video asset ids owned by the calling user.
- If the motion asset legitimately belongs to a partner, have the owner upload/own it or transfer ownership first.
- 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
- Scope the motion-asset picker to assets owned by the current user only.
- Reject cross-user ids at the client boundary.
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
- Live photo video not found
- Live photo video must be a video
- Editing live photos is not supported
- You may not access another user's locked timeline
- Admin status can only be changed by another admin
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/c461e7ff7cb43b1b.
Report an issue: GitHub.