immich-app/immich · warning · NotFoundException
Not Found
Error message
Not Found
What it means
Thrown by PersonService.getThumbnail when personRepository.getById(id) returns null or the person has no thumbnailPath. NotFoundException with no message -> HTTP 404 with body 'Not Found'. Used after the permission check passes, so it indicates the thumbnail file is missing even though the person is accessible.
Solutions
- Re-run the thumbnail generation queue for faces (Administration > Jobs > Thumbnail Generation).
- Verify the storage volume is mounted and the thumbnail path on disk is readable.
- Provide a message to the NotFoundException so clients can distinguish 'no thumbnail' from 'no person'.
Example fix
// before
const person = await this.personRepository.getById(id);
if (!person || !person.thumbnailPath) {
throw new NotFoundException();
}
// after
const person = await this.personRepository.getById(id);
if (!person) {
throw new NotFoundException(`Person ${id} not found`);
}
if (!person.thumbnailPath) {
throw new NotFoundException(`Person ${id} has no thumbnail`);
} Defensive patterns
Strategy: validation
Validate before calling
// Before requesting the thumbnail, verify the person has one.
const person = await personService.getById(auth, id);
if (!person || !person.hasThumbnail) {
// render a placeholder avatar instead of calling the thumbnail endpoint
return PLACEHOLDER_AVATAR;
} Type guard
const hasThumbnail = (p: PersonResponseDto | null | undefined): boolean => !!p && (!!p.thumbnailPath || !!p.hasThumbnail);
Try / catch
try {
return await personService.getThumbnail(auth, id);
} catch (e) {
if (e instanceof NotFoundException) {
// 404 from this endpoint means no thumbnail file; fall back to placeholder
return PLACEHOLDER_AVATAR;
}
throw e;
} Prevention
- Run the Thumbnail Generation job after facial recognition changes.
- Verify storage mounts are present after migrations.
- Add a message to the NotFoundException to distinguish missing-person from missing-thumbnail.
When it happens
Trigger: GET /people/{id}/thumbnail for a person whose thumbnail was never generated, was deleted from disk, or whose thumbnailPath column is empty.
Common situations: Thumbnail generation job failed or was interrupted; storage path was migrated/cleaned; the person was created manually (no face) and never had a thumbnail; cold migration left files behind.
Related errors
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-08-21).
Data as JSON: /api/errors/fbeed7719c1b7764.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/person.service.ts:179
}
}
await this.jobRepository.queueAll(jobs);
}
async getById(auth: AuthDto, personGroupId: string): Promise<PersonResponseDto> {
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [personGroupId] });
return mapPerson(await this.findOrFail(auth, personGroupId));
}
async getStatistics(auth: AuthDto, personGroupId: string): Promise<PersonStatisticsResponseDto> {
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [personGroupId] });
return this.personRepository.getStatistics(personGroupId, auth.user.id);
}
async getThumbnail(auth: AuthDto, personGroupId: string): Promise<ImmichFileResponse> {
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [personGroupId] });
const person = await this.personRepository.getByGroupId({ ownerId: auth.user.id, personGroupId });
if (!person || !person.thumbnailPath) {
throw new NotFoundException();
}
return new ImmichFileResponse({
path: person.thumbnailPath,
contentType: mimeTypes.lookup(person.thumbnailPath),
cacheControl: CacheControl.PrivateWithoutCache,
});
}
async create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
const group = await this.personRepository.createGroup(auth.user.id);
const person = await this.personRepository.create({
ownerId: auth.user.id,
personGroupId: group.id,
name: dto.name,
birthDate: dto.birthDate,View on GitHub (pinned to f48d4b3321)