{"record":{"id":"22e91dfe658bcac9","repo":"immich-app/immich","slug":"sidecar-files-cannot-be-deleted","errorCode":null,"errorMessage":"Sidecar files cannot be deleted","messagePattern":"Sidecar files cannot be deleted","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/asset-file.service.ts","lineNumber":42,"sourceCode":"  async download(auth: AuthDto, id: string) {\n    await this.requireAccess({ auth, permission: Permission.AssetFileDownload, ids: [id] });\n    const file = await findOrFail(() => this.assetFileRepository.get(id), 'Asset file');\n\n    return new ImmichFileResponse({\n      path: file.path,\n      fileName: getFileNameWithoutExtension(file.path) + getFilenameExtension(file.path),\n      contentType: mimeTypes.lookup(file.path),\n      cacheControl: CacheControl.PrivateWithCache,\n    });\n  }\n\n  async delete(auth: AuthDto, id: string) {\n    await this.requireAccess({ auth, permission: Permission.AssetFileDelete, ids: [id] });\n\n    const file = await findOrFail(() => this.assetFileRepository.get(id), 'Asset file');\n    // TODO consider implications of allowing sidecar files to be deleted\n    if (file.type === AssetFileType.Sidecar) {\n      throw new BadRequestException('Sidecar files cannot be deleted');\n    }\n\n    await this.assetFileRepository.delete(id);\n    await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [file.path] } });\n  }\n}\n","sourceCodeStart":24,"sourceCodeEnd":49,"githubUrl":"https://github.com/immich-app/immich/blob/37e033a09d212fca3d273990f138112abb9e0837/server/src/services/asset-file.service.ts#L24-L49","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — deletes whatever the search returned, including sidecars\nfor (const file of await searchAssetFiles({})) {\n  await deleteAssetFile(auth, file.id); // 400 when file.type === 'sidecar'\n}\n\n// after — skip sidecar files up front\nfor (const file of await searchAssetFiles({})) {\n  if (file.type !== 'sidecar') {\n    await deleteAssetFile(auth, file.id);\n  }\n}","handlingStrategy":"type-guard","validationCode":"// Before deleting: resolve the file and skip sidecars.\nconst file = await api.getAssetFile(id); // GET /asset-files/{id}\nif (file.type === 'sidecar') {\n  // handle via the asset metadata/sidecar flow instead\n  return;\n}\nawait api.deleteAssetFile(id); // DELETE /asset-files/{id}","typeGuard":"enum AssetFileType { Sidecar = 'sidecar' }\n\ntype DeletableAssetFile = { id: string; type: string };\n\nconst isDeletableAssetFile = (file: DeletableAssetFile): boolean =>\n  file.type !== AssetFileType.Sidecar;","tryCatchPattern":"for (const file of files) {\n  try {\n    await deleteAssetFile(auth, file.id);\n  } catch (error) {\n    if (isHttpError(error, 400, 'Sidecar files cannot be deleted')) {\n      continue; // expected for sidecar rows in bulk cleanup — log and move on\n    }\n    throw error;\n  }\n}","preventionTips":["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."],"tags":["immich","sidecar","xmp","asset-files","business-rule","http-400","file-management"],"backgroundTag":"protected-resource-delete","analyzedSha":"37e033a09d212fca3d273990f138112abb9e0837","analyzedAt":"2026-08-21T18:08:19.313Z","contentChangedAt":"2026-08-21T18:08:19.313Z","schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}