immich-app/immich · error · BadRequestException

Sidecar files cannot be deleted

Error message

Sidecar files cannot be deleted

What it means

Immich's AssetFileService.delete (DELETE /asset-files/:id, requires Permission.AssetFileDelete) hard-blocks deletion of any asset_file row whose type is AssetFileType.Sidecar — the XMP/XML metadata companion files. The guard is deliberate (a TODO in the source notes the implications are still unsettled): sidecars are owned by the asset's metadata lifecycle, so deleting the row would desynchronize a live asset from its sidecar file on disk. It throws BadRequestException, i.e. HTTP 400.

Source

Thrown at server/src/services/asset-file.service.ts:42

  async download(auth: AuthDto, id: string) {
    await this.requireAccess({ auth, permission: Permission.AssetFileDownload, ids: [id] });
    const file = await findOrFail(() => this.assetFileRepository.get(id), 'Asset file');

    return new ImmichFileResponse({
      path: file.path,
      fileName: getFileNameWithoutExtension(file.path) + getFilenameExtension(file.path),
      contentType: mimeTypes.lookup(file.path),
      cacheControl: CacheControl.PrivateWithCache,
    });
  }

  async delete(auth: AuthDto, id: string) {
    await this.requireAccess({ auth, permission: Permission.AssetFileDelete, ids: [id] });

    const file = await findOrFail(() => this.assetFileRepository.get(id), 'Asset file');
    // TODO consider implications of allowing sidecar files to be deleted
    if (file.type === AssetFileType.Sidecar) {
      throw new BadRequestException('Sidecar files cannot be deleted');
    }

    await this.assetFileRepository.delete(id);
    await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [file.path] } });
  }
}

View on GitHub (pinned to 37e033a09d)

Solutions

  1. Filter out sidecar rows before calling delete — only original/preview/thumbnail (etc.) files are deletable via this endpoint.
  2. To actually remove a sidecar, go through the asset's metadata flow instead: delete the .xmp from the library storage and let Immich's metadata refresh reconcile, or use the asset-level sidecar operations — not the asset-files delete endpoint.
  3. If you administer the instance directly and truly need the row gone, understand the TODO: deleting it leaves an orphaned file on disk and a possibly stale asset metadata state; prefer the supported flow first.

Example fix

// before — deletes whatever the search returned, including sidecars
for (const file of await searchAssetFiles({})) {
  await deleteAssetFile(auth, file.id); // 400 when file.type === 'sidecar'
}

// after — skip sidecar files up front
for (const file of await searchAssetFiles({})) {
  if (file.type !== 'sidecar') {
    await deleteAssetFile(auth, file.id);
  }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Before deleting: resolve the file and skip sidecars.
const file = await api.getAssetFile(id); // GET /asset-files/{id}
if (file.type === 'sidecar') {
  // handle via the asset metadata/sidecar flow instead
  return;
}
await api.deleteAssetFile(id); // DELETE /asset-files/{id}

Type guard

enum AssetFileType { Sidecar = 'sidecar' }

type DeletableAssetFile = { id: string; type: string };

const isDeletableAssetFile = (file: DeletableAssetFile): boolean =>
  file.type !== AssetFileType.Sidecar;

Try / catch

for (const file of files) {
  try {
    await deleteAssetFile(auth, file.id);
  } catch (error) {
    if (isHttpError(error, 400, 'Sidecar files cannot be deleted')) {
      continue; // expected for sidecar rows in bulk cleanup — log and move on
    }
    throw error;
  }
}

Prevention

When it happens

Trigger: Calling DELETE /asset-files/{id} where the file record has type === 'sidecar' (a .xmp next to the asset). Bulk cleanup scripts that search asset-files and delete every row they can read are the classic trigger; the search endpoint (GET /asset-files) happily returns sidecar rows, and delete then rejects them.

Common situations: Storage-cleanup tooling iterating asset_file rows to free space and tripping over sidecars. Confusion between file types (original/preview/thumbnail are deletable, sidecar is not). Attempting to remove an unwanted XMP by deleting its database record instead of through the asset's sidecar flow.

Related errors


AI-assisted analysis of immich-app/immich@37e033a09d (2026-08-21). Data as JSON: /api/errors/22e91dfe658bcac9. Report an issue: GitHub.