{"record":{"id":"2f4d3826adc07c3a","repo":"mudler/LocalAI","slug":"export-failed","errorCode":null,"errorMessage":"Export failed","messagePattern":"Export failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"core/http/react-ui/src/pages/Skills.jsx","lineNumber":97,"sourceCode":"      danger: true,\n      onConfirm: async () => {\n        setConfirmDialog(null)\n        try {\n          await skillsApi.delete(name, userId)\n          addToast(t('toasts.deleted', { name }), 'success')\n          fetchSkills()\n        } catch (err) {\n          addToast(err.message || t('toasts.deleteFailed'), 'error')\n        }\n      },\n    })\n  }\n\n  const exportSkill = async (name, userId) => {\n    try {\n      const url = skillsApi.exportUrl(name, userId)\n      const res = await fetch(url, { credentials: 'same-origin' })\n      if (!res.ok) throw new Error(res.statusText || 'Export failed')\n      const blob = await res.blob()\n      const a = document.createElement('a')\n      a.href = URL.createObjectURL(blob)\n      a.download = `${name.replace(/\\//g, '-')}.tar.gz`\n      document.body.appendChild(a)\n      a.click()\n      document.body.removeChild(a)\n      URL.revokeObjectURL(a.href)\n      addToast(t('toasts.exported', { name }), 'success')\n    } catch (err) {\n      addToast(err.message || t('toasts.exportFailed'), 'error')\n    }\n  }\n\n  const handleImport = async (e) => {\n    const file = e.target.files?.[0]\n    if (!file) return\n    setImporting(true)","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/mudler/LocalAI/blob/44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26/core/http/react-ui/src/pages/Skills.jsx#L79-L115","documentation":"Thrown by exportSkill() when the GET request to the skill export URL (skillsApi.exportUrl(name, userId), credentials same-origin) returns a non-OK status. The message is res.statusText or the literal 'Export failed' when statusText is empty (HTTP/2 responses have no statusText). The fetch downloads a .tar.gz blob and triggers a synthetic anchor click to save it.","triggerScenarios":"GET /api/skills/.../export returning 404 (skill renamed/deleted between list and export), 401/403 (session cookie expired — hence credentials: 'same-origin'), or 500 when tarball creation fails server-side.","commonSituations":"Clicking export on a stale list after the skill was removed; auth cookie expired; skill directory unreadable on the server (permissions); HTTP/2 behind a proxy making statusText empty so every failure shows the generic 'Export failed'.","solutions":["Re-fetch the skills list and retry export on a fresh entry","If auth is enabled, log in again (same-origin cookie may have expired)","Check the network response status/body in devtools for the real reason","If permissions, fix read access on the skills directory server-side"],"exampleFix":"// before\nif (!res.ok) throw new Error(res.statusText || 'Export failed')\n\n// after: include the numeric status, which is never empty\nif (!res.ok) throw new Error(`Export failed: HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`)","handlingStrategy":"retry","validationCode":"// Confirm the skill still exists before offering/attempting export\nasync function skillExists(api, name, userId) {\n  try {\n    const res = await fetch(api.listUrl(userId))\n    if (!res.ok) return false\n    const { skills = [] } = await res.json()\n    return skills.some(s => s.name === name)\n  } catch { return false }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const res = await fetch(url, { credentials: 'same-origin' })\n  if (!res.ok) throw new Error(`Export failed: HTTP ${res.status}`)\n  const blob = await res.blob()\n  if (blob.size === 0) throw new Error('Export returned an empty file')\n  // ...trigger download\n} catch (err) {\n  addToast(err.message || t('toasts.exportFailed'), 'error')\n}","preventionTips":["Include the numeric HTTP status in the thrown message — statusText is empty on HTTP/2","Send credentials explicitly (as this code does) and refresh the list before exporting stale entries","Verify the downloaded blob is non-empty and has a gzip/tar signature before declaring success"],"tags":["http","skills","export","react-ui"],"backgroundTag":null,"analyzedSha":"44413a9d06bf5bc52ce088ba8ca74e5a2e8bee26","analyzedAt":"2026-08-15T10:13:50.291Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}