immich-app/immich · error · BadRequestException

assetIds, albumId, or userId is required

Error message

assetIds, albumId, or userId is required

What it means

getDownloadInfo requires exactly one of assetIds, albumId, or userId in the DTO to determine which assets to include in the download archive. When none is provided, no asset source can be selected and a 400 BadRequestException is thrown.

Solutions

  1. Include exactly one of assetIds (array), albumId, or userId in the request body.
  2. If downloading an album, send {"albumId":"<uuid>"}; for selected assets send assetIds; for a user's library send userId.
  3. Fix the client to disable the download action until a selection/album is chosen.

Example fix

// before
await api.post('/download/info', {});
// after
await api.post('/download/info', { albumId: album.id });
Defensive patterns

Strategy: validation

Validate before calling

const sources = ['assetIds', 'albumId', 'userId'].filter((k) => dto[k] != null && (k !== 'assetIds' || dto[k].length));
if (sources.length !== 1) throw new Error('Download info requires exactly one of assetIds, albumId, or userId');

Try / catch

try {
  await api.post('/download/info', dto);
} catch (e) {
  if (e.status === 400 && /assetIds, albumId, or userId/.test(e.message)) {
    console.error('Add a selection to the download request body');
  } else throw e;
}

Prevention

When it happens

Trigger: POST /download/info with an empty body or a DTO missing all three identifiers — e.g. calling the API directly without a body, or a client UI bug clearing the selection before requesting the archive.

Common situations: Manual API/curl calls omitting the payload; frontend sending only filters like albumName not supported by the DTO; empty multi-select download attempts.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/c9fa760bc80124d1. Report an issue: GitHub.

Appendix: source

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

@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 e55ac299a4)