immich-app/immich · warning · BadRequestException

Either `query` or `queryAssetId` must be set

Error message

Either `query` or `queryAssetId` must be set

What it means

Thrown by SearchService.searchSmart when neither dto.query nor dto.queryAssetId is provided. The smart-search endpoint needs a source for the embedding (free text or an anchor asset); with neither, no embedding can be produced. BadRequestException -> HTTP 400.

Source

Thrown at server/src/services/search.service.ts:174

      const key = machineLearning.clip.modelName + dto.query + dto.language;
      embedding = this.embeddingCache.get(key);
      if (!embedding) {
        embedding = await this.machineLearningRepository.encodeText(dto.query, {
          modelName: machineLearning.clip.modelName,
          language: dto.language,
        });
        this.embeddingCache.set(key, embedding);
      }
    } else if (dto.queryAssetId) {
      await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.queryAssetId] });
      const getEmbeddingResponse = await this.searchRepository.getEmbedding(dto.queryAssetId);
      const assetEmbedding = getEmbeddingResponse?.embedding;
      if (!assetEmbedding) {
        throw new BadRequestException(`Asset ${dto.queryAssetId} has no embedding`);
      }
      embedding = assetEmbedding;
    } else {
      throw new BadRequestException('Either `query` or `queryAssetId` must be set');
    }
    const page = dto.page ?? 1;
    const size = dto.size || 100;
    const { hasNextPage, items } = await this.searchRepository.searchSmart(
      { page, size },
      {
        ...dto,
        userIds: await userIds,
        embedding,
        visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
      },
    );

    return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
  }

  async getAssetsByCity(auth: AuthDto): Promise<AssetResponseDto[]> {
    const userIds = await this.getUserIdsToSearch(auth);

View on GitHub (pinned to 199723261c)

Solutions

  1. Require at least one of query or queryAssetId in the DTO validation before calling the service.
  2. Disable the submit button client-side until one field is non-empty.
  3. Use class-validator @ValidateIf or a custom DTO decorator to enforce one-of at the boundary.

Example fix

// before
} else {
  throw new BadRequestException('Either `query` or `queryAssetId` must be set');
}

// DTO-level validation (preferred)
@ValidateIf((o) => !o.queryAssetId)
@IsNotEmpty()
@IsString()
query?: string;

@ValidateIf((o) => !o.query)
@IsNotEmpty()
@IsString()
queryAssetId?: string;
Defensive patterns

Strategy: validation

Validate before calling

// Require one of query / queryAssetId before calling the service.
if (!dto.query && !dto.queryAssetId) {
  throw new Error('Smart search requires a query or queryAssetId');
}
await searchService.searchSmart(auth, dto);

Type guard

const hasSmartSearchInput = (d: SmartSearchDto): boolean =>
  (!!d.query && d.query.trim().length > 0) || !!d.queryAssetId;

Prevention

When it happens

Trigger: POST /search/smart with an empty body or only filter fields (page, size, visibility) but no `query` and no `queryAssetId`.

Common situations: Client submits the search form before the user typed anything and with no asset selected; DTO validation relaxed at the boundary; a 'search' button wired to fire on focus loss.

Related errors


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