{"record":{"id":"de50fa3fb471cf54","repo":"immich-app/immich","slug":"asset-not-in-stack","errorCode":null,"errorMessage":"Asset not in stack","messagePattern":"Asset not in stack","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/stack.service.ts","lineNumber":70,"sourceCode":"    await this.requireAccess({ auth, permission: Permission.StackDelete, ids: [id] });\n    await this.stackRepository.delete(id);\n    await this.eventRepository.emit('StackDelete', { stackId: id, userId: auth.user.id });\n  }\n\n  async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {\n    await this.requireAccess({ auth, permission: Permission.StackDelete, ids: dto.ids });\n    await this.stackRepository.deleteAll(dto.ids);\n    await this.eventRepository.emit('StackDeleteAll', { stackIds: dto.ids, userId: auth.user.id });\n  }\n\n  async removeAsset(auth: AuthDto, dto: UUIDAssetIDParamDto): Promise<void> {\n    const { id: stackId, assetId } = dto;\n    await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [stackId] });\n\n    const stack = await this.stackRepository.getForAssetRemoval(assetId);\n\n    if (!stack?.id || stack.id !== stackId) {\n      throw new BadRequestException('Asset not in stack');\n    }\n\n    if (stack.primaryAssetId === assetId) {\n      throw new BadRequestException(\"Cannot remove stack's primary asset\");\n    }\n\n    await this.assetRepository.update({ id: assetId, stackId: null });\n    await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id });\n  }\n\n  private findOrFail(id: string) {\n    return findOrFail(() => this.stackRepository.getById(id), 'Asset stack');\n  }\n}\n","sourceCodeStart":52,"sourceCodeEnd":85,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/stack.service.ts#L52-L85","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before - stale ids\nawait removeAsset({ id: oldStackId, assetId });\n// after - confirm current membership\nconst stack = await getStackForAsset(assetId);\nif (stack?.id) await removeAsset({ id: stack.id, assetId });","handlingStrategy":"validation","validationCode":"const stack = await findStackForAsset(assetId);\nif (!stack || stack.id !== stackId) {\n  // asset is not in this stack; treat as already-removed\n  return;\n}\nawait stackApi.removeAsset({ id: stackId, assetId });","typeGuard":"const assetIsInStack = (assetId: string, stackId: string, stack: { id: string; assets: { id: string }[] } | null): boolean =>\n  !!stack && stack.id === stackId && stack.assets.some((a) => a.id === assetId);","tryCatchPattern":"try {\n  await stackApi.removeAsset({ id: stackId, assetId });\n} catch (e) {\n  if (e instanceof BadRequestException && /not in stack/i.test(e.message)) {\n    // benign: already removed or belongs elsewhere\n    return;\n  } else throw e;\n}","preventionTips":["Refresh stack membership before issuing remove actions.","Treat 'not in stack' as a benign no-op on the client."],"tags":["stack","asset","validation"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}