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
- 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.
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
- 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.
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
- Invalid job name
- Library ${id} not found
- Invalid import path: ${path.message}
- Notification not found
- User not found
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/c9fa760bc80124d1.
Report an issue: GitHub.