{"record":{"id":"c9fa760bc80124d1","repo":"immich-app/immich","slug":"assetids-albumid-or-userid-is-required","errorCode":null,"errorMessage":"assetIds, albumId, or userId is required","messagePattern":"assetIds, albumId, or userId is required","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/download.service.ts","lineNumber":31,"sourceCode":"@Injectable()\nexport class DownloadService extends BaseService {\n  async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {\n    let assets;\n\n    if (dto.assetIds) {\n      const assetIds = dto.assetIds;\n      await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: assetIds });\n      assets = this.downloadRepository.downloadAssetIds(assetIds);\n    } else if (dto.albumId) {\n      const albumId = dto.albumId;\n      await this.requireAccess({ auth, permission: Permission.AlbumDownload, ids: [albumId] });\n      assets = this.downloadRepository.downloadAlbumId(albumId);\n    } else if (dto.userId) {\n      const userId = dto.userId;\n      await this.requireAccess({ auth, permission: Permission.TimelineDownload, ids: [userId] });\n      assets = this.downloadRepository.downloadUserId(userId);\n    } else {\n      throw new BadRequestException('assetIds, albumId, or userId is required');\n    }\n\n    const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;\n    const metadata = await this.userRepository.getMetadata(auth.user.id);\n    const preferences = getPreferences(metadata);\n    const motionIds = new Set<string>();\n    const archives: DownloadArchiveInfo[] = [];\n    let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };\n\n    const addToArchive = ({ id, size }: { id: string; size: number | null }) => {\n      archive.assetIds.push(id);\n      archive.size += Number(size || 0);\n\n      if (archive.size > targetSize) {\n        archives.push(archive);\n        archive = { size: 0, assetIds: [] };\n      }\n    };","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/download.service.ts#L13-L49","documentation":"The download endpoint (DownloadService.getDownloadInfo) builds an archive from one of three sources: a list of assets, an album, or a user's timeline. The DTO is structured so all three fields are optional individually, but exactly one must be supplied. If none is present the service cannot determine what to package and rejects the request with a 400 BadRequestException.","triggerScenarios":"GET/POST to /download/info (or the archive endpoint) with a DownloadInfoDto where dto.assetIds, dto.albumId, and dto.userId are all undefined/empty. Triggered by a frontend bug, a hand-crafted curl call, or a client that cleared its selection before requesting a download.","commonSituations":"UI race where the user clicks Download with nothing selected; a script that posts an empty body; mobile client losing the selection state before the request fires.","solutions":["Send at least one of assetIds (non-empty array), albumId, or userId in the request body/query.","On the client, disable the Download action until a valid target is selected.","If calling manually, e.g.: curl -X POST .../download/info -d '{\"assetIds\":[\"<uuid>\"]}'.","Check the API DTO (DownloadInfoDto) to confirm the exact field names and types expected."],"exampleFix":"// before\nconst res = await api.downloadApi.getDownloadInfo({});\n// after\nconst res = await api.downloadApi.getDownloadInfo({ assetIds: selectedIds });","handlingStrategy":"validation","validationCode":"// client-side: ensure exactly one target before calling\nfunction assertDownloadTarget(dto: DownloadInfoDto) {\n  const provided = [dto.assetIds?.length, dto.albumId, dto.userId].filter(Boolean).length;\n  if (provided !== 1) throw new Error('Provide exactly one of assetIds, albumId, or userId');\n}\nassertDownloadTarget(dto);\nawait api.downloadApi.getDownloadInfo(dto);","typeGuard":"type DownloadInfoDto = { assetIds?: string[]; albumId?: string; userId?: string };\nconst hasDownloadTarget = (d: DownloadInfoDto): boolean =>\n  Boolean(d.assetIds?.length || d.albumId || d.userId);","tryCatchPattern":"try {\n  await api.downloadApi.getDownloadInfo(dto);\n} catch (e) {\n  if (e.status === 400 && /is required/.test(e.message)) {\n    notifyUser('Select at least one asset, album, or user to download.');\n  } else throw e;\n}","preventionTips":["Disable the Download UI action until a target is selected.","Validate the request body with a schema (class-validator/zod) on the client.","Write integration tests covering each of the three input shapes."],"tags":["api","validation","download","nestjs","bad-request","immich","typescript"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}