immich-app/immich · error · BadRequestException

Invalid assetId for feature face or asset is offline

Error message

Invalid assetId for feature face or asset is offline

What it means

Thrown by PersonService.update when dto.featureFaceAssetId is set but personRepository.getForFeatureFaceUpdate({ personId, assetId }) returns null. The query requires the asset to be readable by the caller AND contain a face assigned to this person AND the asset file to be present (not offline). BadRequestException -> HTTP 400.

Source

Thrown at server/src/services/person.service.ts:199

      isHidden: dto.isHidden,
      isFavorite: dto.isFavorite,
      color: dto.color,
    });

    return mapPerson(person);
  }

  async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
    await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] });

    const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
    // TODO: set by faceId directly
    let faceId: string | undefined;
    if (assetId) {
      await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] });
      const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });
      if (!face) {
        throw new BadRequestException('Invalid assetId for feature face or asset is offline');
      }

      faceId = face.id;
    }

    const person = await this.personRepository.update({
      id,
      faceAssetId: faceId,
      name,
      birthDate,
      isHidden,
      isFavorite,
      color,
    });

    if (assetId) {
      await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id } });
    }

View on GitHub (pinned to 199723261c)

Solutions

  1. Use an asset that visibly contains a face for this person (check the asset's people/faces list first).
  2. Re-run face detection on the asset so a face row exists.
  3. If the asset is offline, restore the file to its storage path before selecting it.

Example fix

// before
const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });
if (!face) {
  throw new BadRequestException('Invalid assetId for feature face or asset is offline');
}

// after (split the two distinct failure modes)
const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });
if (!face) {
  const asset = await this.assetRepository.getById(assetId);
  throw new BadRequestException(
    asset?.isOffline ? 'Asset is offline' : 'Asset has no face for this person',
  );
}
Defensive patterns

Strategy: validation

Validate before calling

// Before selecting a feature face, confirm the asset has a face for this person.
const faces = await assetService.getFaces(auth, assetId);
if (!faces.some((f) => f.personId === personId)) {
  throw new Error('Cannot use this asset: no face for the target person');
}
await personService.update(auth, personId, { featureFaceAssetId: assetId });

Type guard

const assetHasFaceForPerson = (faces: AssetFace[], personId: string): boolean =>
  faces.some((f) => f.personId === personId);

Try / catch

try {
  await personService.update(auth, id, { featureFaceAssetId: assetId });
} catch (e) {
  if (e instanceof BadRequestException && /offline|feature face/i.test(e.message)) {
    // tell the user to re-run face detection or pick another asset
  }
  throw e;
}

Prevention

When it happens

Trigger: PUT /people/{id} with featureFaceAssetId pointing at an asset that has no face for this person, an asset that is offline (file missing from storage), or an asset the user can read but that lacks a usable face.

Common situations: Selecting a feature face from an asset after the face was reassigned to another person; the asset's original file was moved offline; choosing an asset from before re-running face detection that has no detected face.

Related errors


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