{"record":{"id":"c1804d4fa9cd20eb","repo":"infiniflow/ragflow","slug":"failed-to-delete-skill","errorCode":null,"errorMessage":"Failed to delete skill","messagePattern":"Failed to delete skill","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/pages/skills/hooks.ts","lineNumber":1123,"sourceCode":"              await skillSpaceService.deleteSkillIndex(\n                indexId,\n                normalizedSpaceId,\n              );\n              // oxlint-disable-next-line no-console\n              console.log(\n                `[deleteSkill] Successfully deleted index: ${indexId}`,\n              );\n            } catch {\n              // Ignore errors for versions that don't exist\n            }\n          }\n        }\n\n        const { data } = await fileManagerService.removeFile({\n          ids: [targetFolderId],\n        });\n\n        if (data.code !== 0) throw new Error('Failed to delete skill');\n\n        message.success(t('skills.deleteSuccess'));\n        // Refresh skills list using space name and space id\n        await fetchSkills(normalizedSpaceName, normalizedSpaceId);\n        return true;\n      } catch (error) {\n        console.error('Error deleting skill:', error);\n        message.error(t('skills.deleteError'));\n        return false;\n      }\n    },\n    // oxlint-disable-next-line react/exhaustive-deps\n    [t, fetchSkills, ensureSkillSpaceFolder, skills],\n  );\n\n  // Recursively find file by path in folder structure\n  // For versioned skills, automatically finds the version folder first\n  const findFileByPath = async (","sourceCodeStart":1105,"sourceCodeEnd":1141,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/web/src/pages/skills/hooks.ts#L1105-L1141","documentation":"Thrown in web/src/pages/skills/hooks.ts:1123 at the final step of deleteSkill: after resolving the folder, cleaning up per-version index entries (whose errors are deliberately ignored), the actual removeFile({ ids: [targetFolderId] }) returns code !== 0. The folder delete was rejected server-side; the outer catch converts it into a generic 'skills.deleteError' toast and returns false.","triggerScenarios":"The folder was deleted by another session between resolution and removeFile (stale targetFolderId); the folder contains files the backend refuses to cascade-delete; permission loss; file-manager service error. Note the index-cleanup loop before it swallows its own errors, so index state may already be partially modified when this fires.","commonSituations":"Double-delete from two tabs. Shared space where another user reorganized folders. Backend restart between steps. Folder containing large uploads timing out server-side.","solutions":["Distinguish 'already deleted' (retry the listFile for targetFolderId; if gone, treat as success) from real failures","Surface data.message from removeFile in the error/log instead of the generic string","Re-fetch the skills list even on failure so UI reflects actual state","Disable the delete button while a deletion is in flight to prevent double submits"],"exampleFix":"// before\nconst { data } = await fileManagerService.removeFile({\n  ids: [targetFolderId],\n});\nif (data.code !== 0) throw new Error('Failed to delete skill');\n\n// after\nconst { data } = await fileManagerService.removeFile({\n  ids: [targetFolderId],\n});\nif (data.code !== 0) {\n  const { data: recheck } = await fileManagerService.listFile({\n    parent_id: spaceFolderId ?? targetFolderId,\n  });\n  const stillThere = (recheck.data?.files || []).some(\n    (f: any) => f.id === targetFolderId,\n  );\n  if (!stillThere) {\n    await fetchSkills(normalizedSpaceName, normalizedSpaceId);\n    return true;\n  }\n  throw new Error(data.message || 'Failed to delete skill');\n}","handlingStrategy":"try-catch","validationCode":"const folderStillExists = async (parentId: string, folderId: string) =>\n  (await fileManagerService.listFile({ parent_id: parentId }))\n    .data.data?.files?.some((f) => f.id === folderId) ?? false;","typeGuard":null,"tryCatchPattern":"try {\n  const { data } = await fileManagerService.removeFile({ ids: [folderId] });\n  if (data.code !== 0) throw new Error(data.message);\n} catch (e) {\n  if (!(await folderStillExists(parent, folderId))) return true; // already gone\n  throw e;\n}","preventionTips":["Disable the delete button while a deletion is in flight (double-submit guard)","Always refetch the skills list after delete attempts, success or failure"],"tags":["skills","file-manager","delete","api-contract"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}