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
- Filter out sidecar rows before calling delete — only original/preview/thumbnail (etc.) files are deletable via this endpoint.
- 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.
- 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 enumerating asset files for cleanup, filter type === 'sidecar' out before issuing deletes.
- Use the asset's metadata refresh / sidecar operations to manage XMP lifecycle, never the asset-files delete endpoint.
- Remember the rule: previews/thumbnails/originals are deletable; sidecars are not.
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
- Cannot request to join your own cluster group
- Cannot leave a cluster group without any other members
- Cannot rotate an API Key with permissions you do not have
- The ${name} extension is not available in this Postgres inst
- The ${name} extension version is ${version}, which mean
AI-assisted analysis of immich-app/immich@37e033a09d (2026-08-21).
Data as JSON: /api/errors/22e91dfe658bcac9.
Report an issue: GitHub.