{"record":{"id":"a117ce0fb7971576","repo":"Stirling-Tools/Stirling-PDF","slug":"failed-to-save-file","errorCode":null,"errorMessage":"Failed to save file","messagePattern":"Failed to save file","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/desktop/services/downloadService.ts","lineNumber":18,"sourceCode":"import type {\n  DownloadRequest,\n  DownloadResult,\n} from \"@core/services/downloadService\";\nimport {\n  saveToLocalPath,\n  showSaveDialog,\n} from \"@app/services/localFileSaveService\";\n\nexport type { DownloadRequest, DownloadResult };\n\nexport async function downloadFile(\n  request: DownloadRequest,\n): Promise<DownloadResult> {\n  if (request.localPath) {\n    const result = await saveToLocalPath(request.data, request.localPath);\n    if (!result.success) {\n      throw new Error(result.error || \"Failed to save file\");\n    }\n    return { savedPath: request.localPath };\n  }\n\n  const savePath = await showSaveDialog(request.filename);\n  if (!savePath) {\n    return { cancelled: true };\n  }\n\n  const result = await saveToLocalPath(request.data, savePath);\n  if (!result.success) {\n    throw new Error(result.error || \"Failed to save file\");\n  }\n\n  return { savedPath: savePath };\n}\n\nexport async function downloadFromUrl(","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/desktop/services/downloadService.ts#L1-L36","documentation":"Thrown by downloadFile() when request.localPath is set (a pre-chosen destination) and saveToLocalPath() returns success:false. The real reason is in result.error (filesystem write failure). This path is used when the caller already knows where to save and bypasses the save dialog.","triggerScenarios":"saveToLocalPath rejects the write for request.localPath: directory does not exist or is not writable, the file is locked by another process, the path is outside the allowed Tauri scope, or the disk is full.","commonSituations":"Caller passed an auto-generated path whose parent dir was deleted; path outside the Tauri fs allow-list (scope config); target file open in another app; read-only location.","solutions":["Inspect result.error before it is wrapped — pre-check the path with exists()/permissions in the caller.","Ensure request.localPath is within the Tauri fs plugin scope (configure allowed directories).","Verify the parent directory exists and is writable before calling downloadFile.","Fall back to the dialog-based flow (omit localPath) if the direct save fails."],"exampleFix":"// before\nawait downloadFile({ data: blob, filename, localPath });\n\n// after: ensure dir exists + scope, else fall back to dialog\nconst dir = await dirname(localPath);\nif (!(await exists(dir))) await mkdir(dir, { recursive: true });\ntry { await downloadFile({ data: blob, filename, localPath }); }\ncatch (e) { await downloadFile({ data: blob, filename }); // dialog fallback }","handlingStrategy":"validation","validationCode":"// ensure parent dir exists and is in scope before the direct save\nimport { exists, mkdir } from '@tauri-apps/plugin-fs';\nconst dir = await dirname(request.localPath);\nif (!(await exists(dir))) await mkdir(dir, { recursive: true });","typeGuard":"function isSaveFailure(e: unknown): e is Error {\n  return e instanceof Error && /Failed to save file/.test(e.message);\n}","tryCatchPattern":"try { await downloadFile({ data: blob, filename, localPath }); }\ncatch (e) {\n  if (isSaveFailure(e)) { await downloadFile({ data: blob, filename }); /* dialog fallback */ return; }\n  throw e;\n}","preventionTips":["Verify the parent directory exists before saving.","Keep localPath inside the Tauri fs plugin scope.","Fall back to the save dialog when a direct path fails."],"tags":["filesystem","download","save","tauri-scope","desktop"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}