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
- Choose an assetId that actually contains a detected face for this person (list the person's assets first).
- Restore the offline asset to its library path and let the server re-index so the face row exists.
- Re-run face detection/recognition if the asset was added but faces were not yet processed.
- 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
- Only choose feature faces from assets returned for that person.
- Ensure all library files are online before re-indexing faces.
- Keep face detection/recognition jobs completed before editing people.
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
- Asset not found
- Asset does not have valid dimensions
- Asset not found
- assetIds, albumId, or userId is required
- At least two people are required for merging
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)