immich-app/immich · warning · BadRequestException
Asset not in stack
Error message
Asset not in stack
What it means
StackService.removeAsset() looks up the stack containing the given assetId via stackRepository.getForAssetRemoval(assetId). If no stack is found, or the found stack's id differs from the requested stackId, it throws BadRequestException 'Asset not in stack' (stack.service.ts:70, HTTP 400).
Source
Thrown at server/src/services/stack.service.ts:70
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);
await this.eventRepository.emit('StackDeleteAll', { stackIds: dto.ids, userId: auth.user.id });
}
async removeAsset(auth: AuthDto, dto: UUIDAssetIDParamDto): Promise<void> {
const { id: stackId, assetId } = dto;
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [stackId] });
const stack = await this.stackRepository.getForAssetRemoval(assetId);
if (!stack?.id || stack.id !== stackId) {
throw new BadRequestException('Asset not in stack');
}
if (stack.primaryAssetId === assetId) {
throw new BadRequestException("Cannot remove stack's primary asset");
}
await this.assetRepository.update({ id: assetId, stackId: null });
await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id });
}
private findOrFail(id: string) {
return findOrFail(() => this.stackRepository.getById(id), 'Asset stack');
}
}
View on GitHub (pinned to 199723261c)
Solutions
- Verify the asset still belongs to the stack before issuing removeAsset (fetch the stack).
- Treat this error as a benign no-op on the client if the asset is already gone.
- Ensure the stackId in the path matches the asset's current stack.
Example fix
// before - stale ids
await removeAsset({ id: oldStackId, assetId });
// after - confirm current membership
const stack = await getStackForAsset(assetId);
if (stack?.id) await removeAsset({ id: stack.id, assetId }); Defensive patterns
Strategy: validation
Validate before calling
const stack = await findStackForAsset(assetId);
if (!stack || stack.id !== stackId) {
// asset is not in this stack; treat as already-removed
return;
}
await stackApi.removeAsset({ id: stackId, assetId }); Type guard
const assetIsInStack = (assetId: string, stackId: string, stack: { id: string; assets: { id: string }[] } | null): boolean =>
!!stack && stack.id === stackId && stack.assets.some((a) => a.id === assetId); Try / catch
try {
await stackApi.removeAsset({ id: stackId, assetId });
} catch (e) {
if (e instanceof BadRequestException && /not in stack/i.test(e.message)) {
// benign: already removed or belongs elsewhere
return;
} else throw e;
} Prevention
- Refresh stack membership before issuing remove actions.
- Treat 'not in stack' as a benign no-op on the client.
When it happens
Trigger: DELETE /stacks/:id/assets/:assetId where the asset is not a member of that stack (it belongs to another stack or to none). The permission check for StackUpdate on stackId passes first.
Common situations: Client showing stale membership after a concurrent change, removing an asset that was already removed, or a URL/route mismatch between stackId and the asset's actual stack.
Related errors
- Primary asset must be in the 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/de50fa3fb471cf54.
Report an issue: GitHub.