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

PersonService.update, when setting a feature face by assetId, first checks read access and then queries getForFeatureFaceUpdate to resolve a face for that (personGroupId, assetId) pair. If no face row is found — the asset has no recognized face for this person, or the asset is offline/not yet indexed — it throws BadRequestException with this message.

Solutions

  1. Choose an assetId that actually contains a detected face for this person (list the person's assets first).
  2. Restore the offline asset to its library path and let the server re-index so the face row exists.
  3. Re-run face detection/recognition if the asset was added but faces were not yet processed.
  4. Confirm the personId matches the person shown in the client to avoid cross-person asset selection.

Example fix

// before
await api.peopleApi.updatePerson({ id: personId, personUpdateDto: { featureFaceAssetId: anyAssetId } });
// after
const assets = await api.peopleApi.getPersonAssets({ id: personId });
if (assets.length > 0) {
  await api.peopleApi.updatePerson({ id: personId, personUpdateDto: { featureFaceAssetId: assets[0].id } });
}
Defensive patterns

Strategy: validation

Validate before calling

const assets = await api.peopleApi.getPersonAssets({ id: personId });
const valid = assets.some((a) => a.id === assetId);
if (!valid) throw new Error('assetId has no face for this person or asset is offline');

Try / catch

try {
  await api.peopleApi.updatePerson({ id: personId, personUpdateDto: { featureFaceAssetId: assetId } });
} catch (e) {
  if (e.status === 400 && String(e.message).includes('Invalid assetId for feature face')) {
    // pick an asset from getPersonAssets or restore offline asset
  } else throw e;
}

Prevention

When it happens

Trigger: PUT /api/people/{id} with an assetId whose asset has no face linked to that person, the asset is offline (not on disk/in library), or the assetId belongs to a different person's faces.

Common situations: Client picked an asset from an old preview before re-indexing, asset file moved/deleted (offline storage), choosing an asset not containing the person, or IDs mixed up after a merge.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

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

    return mapPerson(person);
  }

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

    const { ownerId } = await this.findOrFail(auth, personGroupId);
    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({ personGroupId, assetId });
      if (!face) {
        throw new BadRequestException('Invalid assetId for feature face or asset is offline');
      }

      faceId = face.id;
    }

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

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

View on GitHub (pinned to f48d4b3321)