immich-app/immich · warning · BadRequestException
Primary asset must be in the stack
Error message
Primary asset must be in the stack
What it means
StackService.update() lets you change a stack's primary asset, but the new primaryAssetId must already be a member of that stack. If dto.primaryAssetId is set AND none of stack.assets has that id, it throws BadRequestException 'Primary asset must be in the stack' (stack.service.ts:41, HTTP 400).
Source
Thrown at server/src/services/stack.service.ts:41
const stack = await this.stackRepository.create({ ownerId: auth.user.id }, dto.assetIds);
await this.eventRepository.emit('StackCreate', { stackId: stack.id, userId: auth.user.id });
return mapStack(stack, { auth });
}
async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
await this.requireAccess({ auth, permission: Permission.StackRead, ids: [id] });
const stack = await this.findOrFail(id);
return mapStack(stack, { auth });
}
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] });
const stack = await this.findOrFail(id);
if (dto.primaryAssetId && stack.assets.every(({ id }) => id !== dto.primaryAssetId)) {
throw new BadRequestException('Primary asset must be in the stack');
}
const updatedStack = await this.stackRepository.update(id, { id, primaryAssetId: dto.primaryAssetId });
await this.eventRepository.emit('StackUpdate', { stackId: id, userId: auth.user.id });
return mapStack(updatedStack, { auth });
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.StackDelete, ids: [id] });
await this.stackRepository.delete(id);
await this.eventRepository.emit('StackDelete', { stackId: id, userId: auth.user.id });
}
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.StackDelete, ids: dto.ids });
await this.stackRepository.deleteAll(dto.ids);View on GitHub (pinned to 199723261c)
Solutions
- Ensure the primaryAssetId you send is currently in the stack (fetch GET /stacks/:id first).
- If you need to promote an external asset, add it to the stack before setting it as primary.
- Refresh the client's stack membership view before issuing the update.
Example fix
// before - asset not in stack
await update(stackId, { primaryAssetId: externalAssetId });
// after - promote an existing member
await update(stackId, { primaryAssetId: stack.assets[0].id }); Defensive patterns
Strategy: validation
Validate before calling
const stack = await stackApi.get(id);
const memberIds = new Set(stack.assets.map((a) => a.id));
if (dto.primaryAssetId && !memberIds.has(dto.primaryAssetId)) {
throw new Error('primaryAssetId must be a current member of the stack.');
}
await stackApi.update(id, dto); Type guard
const isStackMember = (id: string, stack: { assets: { id: string }[] }): boolean =>
stack.assets.some((a) => a.id === id); Try / catch
try {
await stackApi.update(id, dto);
} catch (e) {
if (e instanceof BadRequestException && /must be in the stack/i.test(e.message)) {
refreshStackMembership(id);
} else throw e;
} Prevention
- Fetch current stack membership before promoting a primary.
- Add external assets to the stack before setting them as primary.
When it happens
Trigger: PATCH /stacks/:id with a primaryAssetId that is not one of the stack's current asset ids. The check runs after the StackUpdate permission check.
Common situations: Setting primary to an asset id from a different stack, a stale client view of stack membership after assets were added/removed, or passing an asset id that was deleted.
Related errors
- Asset not in stack
- Cannot remove stack's primary asset
- May not request original file
- Asset not found or asset is not a video
- Both assets must exist
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/637b04b69ec7a0f0.
Report an issue: GitHub.