{"record":{"id":"ad00eefa119fb651","repo":"immich-app/immich","slug":"invalid-assetid-for-feature-face-or-asset-is-offli","errorCode":null,"errorMessage":"Invalid assetId for feature face or asset is offline","messagePattern":"Invalid assetId for feature face or asset is offline","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/person.service.ts","lineNumber":199,"sourceCode":"      isHidden: dto.isHidden,\n      isFavorite: dto.isFavorite,\n      color: dto.color,\n    });\n\n    return mapPerson(person);\n  }\n\n  async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {\n    await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] });\n\n    const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;\n    // TODO: set by faceId directly\n    let faceId: string | undefined;\n    if (assetId) {\n      await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] });\n      const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });\n      if (!face) {\n        throw new BadRequestException('Invalid assetId for feature face or asset is offline');\n      }\n\n      faceId = face.id;\n    }\n\n    const person = await this.personRepository.update({\n      id,\n      faceAssetId: faceId,\n      name,\n      birthDate,\n      isHidden,\n      isFavorite,\n      color,\n    });\n\n    if (assetId) {\n      await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id } });\n    }","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/person.service.ts#L181-L217","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use an asset that visibly contains a face for this person (check the asset's people/faces list first).","Re-run face detection on the asset so a face row exists.","If the asset is offline, restore the file to its storage path before selecting it."],"exampleFix":"// before\nconst face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });\nif (!face) {\n  throw new BadRequestException('Invalid assetId for feature face or asset is offline');\n}\n\n// after (split the two distinct failure modes)\nconst face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });\nif (!face) {\n  const asset = await this.assetRepository.getById(assetId);\n  throw new BadRequestException(\n    asset?.isOffline ? 'Asset is offline' : 'Asset has no face for this person',\n  );\n}","handlingStrategy":"validation","validationCode":"// Before selecting a feature face, confirm the asset has a face for this person.\nconst faces = await assetService.getFaces(auth, assetId);\nif (!faces.some((f) => f.personId === personId)) {\n  throw new Error('Cannot use this asset: no face for the target person');\n}\nawait personService.update(auth, personId, { featureFaceAssetId: assetId });","typeGuard":"const assetHasFaceForPerson = (faces: AssetFace[], personId: string): boolean =>\n  faces.some((f) => f.personId === personId);","tryCatchPattern":"try {\n  await personService.update(auth, id, { featureFaceAssetId: assetId });\n} catch (e) {\n  if (e instanceof BadRequestException && /offline|feature face/i.test(e.message)) {\n    // tell the user to re-run face detection or pick another asset\n  }\n  throw e;\n}","preventionTips":["Only offer assets that contain a face for the person in the feature-face picker.","Re-run face detection if the asset list is empty.","Restore offline assets to storage before using them as a feature face source."],"tags":["person","face","asset","facial-recognition","nestjs","validation"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}