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

  1. Ensure the primaryAssetId you send is currently in the stack (fetch GET /stacks/:id first).
  2. If you need to promote an external asset, add it to the stack before setting it as primary.
  3. 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

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


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