immich-app/immich · error · BadRequestException

assetIds, albumId, or userId is required

Error message

assetIds, albumId, or userId is required

What it means

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.

Source

Thrown at server/src/services/download.service.ts:31

@Injectable()
export class DownloadService extends BaseService {
  async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
    let assets;

    if (dto.assetIds) {
      const assetIds = dto.assetIds;
      await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: assetIds });
      assets = this.downloadRepository.downloadAssetIds(assetIds);
    } else if (dto.albumId) {
      const albumId = dto.albumId;
      await this.requireAccess({ auth, permission: Permission.AlbumDownload, ids: [albumId] });
      assets = this.downloadRepository.downloadAlbumId(albumId);
    } else if (dto.userId) {
      const userId = dto.userId;
      await this.requireAccess({ auth, permission: Permission.TimelineDownload, ids: [userId] });
      assets = this.downloadRepository.downloadUserId(userId);
    } else {
      throw new BadRequestException('assetIds, albumId, or userId is required');
    }

    const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
    const metadata = await this.userRepository.getMetadata(auth.user.id);
    const preferences = getPreferences(metadata);
    const motionIds = new Set<string>();
    const archives: DownloadArchiveInfo[] = [];
    let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };

    const addToArchive = ({ id, size }: { id: string; size: number | null }) => {
      archive.assetIds.push(id);
      archive.size += Number(size || 0);

      if (archive.size > targetSize) {
        archives.push(archive);
        archive = { size: 0, assetIds: [] };
      }
    };

View on GitHub (pinned to 199723261c)

Solutions

  1. Send at least one of assetIds (non-empty array), albumId, or userId in the request body/query.
  2. On the client, disable the Download action until a valid target is selected.
  3. If calling manually, e.g.: curl -X POST .../download/info -d '{"assetIds":["<uuid>"]}'.
  4. Check the API DTO (DownloadInfoDto) to confirm the exact field names and types expected.

Example fix

// before
const res = await api.downloadApi.getDownloadInfo({});
// after
const res = await api.downloadApi.getDownloadInfo({ assetIds: selectedIds });
Defensive patterns

Strategy: validation

Validate before calling

// client-side: ensure exactly one target before calling
function assertDownloadTarget(dto: DownloadInfoDto) {
  const provided = [dto.assetIds?.length, dto.albumId, dto.userId].filter(Boolean).length;
  if (provided !== 1) throw new Error('Provide exactly one of assetIds, albumId, or userId');
}
assertDownloadTarget(dto);
await api.downloadApi.getDownloadInfo(dto);

Type guard

type DownloadInfoDto = { assetIds?: string[]; albumId?: string; userId?: string };
const hasDownloadTarget = (d: DownloadInfoDto): boolean =>
  Boolean(d.assetIds?.length || d.albumId || d.userId);

Try / catch

try {
  await api.downloadApi.getDownloadInfo(dto);
} catch (e) {
  if (e.status === 400 && /is required/.test(e.message)) {
    notifyUser('Select at least one asset, album, or user to download.');
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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