mudler/LocalAI · error · Error

HTTP ${r.status}

Error message

HTTP ${r.status}

What it means

Thrown by skillsApi.import when the multipart FormData upload (file field) to POST /api/agents/skills/import returns a non-ok response. Unlike the JSON helpers, this inline fetch only throws the bare HTTP status without reading the error body, so the real server reason (usually a validation error on the skill archive) is hidden behind the status code.

Source

Thrown at core/http/react-ui/src/utils/api.js:472

  entries: (name, userId) => fetchJSON(`/api/agents/collections/${enc(name)}/entries${userQ(userId)}`),
  entryContent: (name, entry, userId) => fetchJSON(`/api/agents/collections/${enc(name)}/entries/${encodeURIComponent(entry)}${userQ(userId)}`),
  search: (name, query, maxResults, userId) => postJSON(`/api/agents/collections/${enc(name)}/search${userQ(userId)}`, { query, max_results: maxResults }),
  reset: (name, userId) => postJSON(`/api/agents/collections/${enc(name)}/reset${userQ(userId)}`),
  deleteEntry: (name, entry, userId) => fetchJSON(`/api/agents/collections/${enc(name)}/entry/delete${userQ(userId)}`, { method: 'DELETE', body: JSON.stringify({ entry }), headers: { 'Content-Type': 'application/json' } }),
  sources: (name, userId) => fetchJSON(`/api/agents/collections/${enc(name)}/sources${userQ(userId)}`),
  addSource: (name, url, interval, userId) => postJSON(`/api/agents/collections/${enc(name)}/sources${userQ(userId)}`, { url, update_interval: interval }),
  removeSource: (name, url, userId) => fetchJSON(`/api/agents/collections/${enc(name)}/sources${userQ(userId)}`, { method: 'DELETE', body: JSON.stringify({ url }), headers: { 'Content-Type': 'application/json' } }),
}

// Skills API
export const skillsApi = {
  list: (allUsers) => fetchJSON(`/api/agents/skills${allUsers ? '?all_users=true' : ''}`),
  search: (q) => fetchJSON(`/api/agents/skills/search?q=${enc(q)}`),
  get: (name, userId) => fetchJSON(`/api/agents/skills/${enc(name)}${userQ(userId)}`),
  create: (data) => postJSON('/api/agents/skills', data),
  update: (name, data, userId) => fetchJSON(`/api/agents/skills/${enc(name)}${userQ(userId)}`, { method: 'PUT', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' } }),
  delete: (name, userId) => fetchJSON(`/api/agents/skills/${enc(name)}${userQ(userId)}`, { method: 'DELETE' }),
  import: (file) => { const fd = new FormData(); fd.append('file', file); return fetch(apiUrl('/api/agents/skills/import'), { method: 'POST', body: fd }).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); },
  exportUrl: (name, userId) => apiUrl(`/api/agents/skills/export/${enc(name)}${userQ(userId)}`),
  listResources: (name, userId) => fetchJSON(`/api/agents/skills/${enc(name)}/resources${userQ(userId)}`),
  getResource: (name, path, opts, userId) => fetchJSON(`/api/agents/skills/${enc(name)}/resources/${path}${opts?.json ? '?encoding=base64' : ''}${userId ? `${opts?.json ? '&' : '?'}user_id=${enc(userId)}` : ''}`),
  createResource: (name, path, file) => { const fd = new FormData(); fd.append('file', file); fd.append('path', path); return fetch(apiUrl(`/api/agents/skills/${enc(name)}/resources`), { method: 'POST', body: fd }).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); },
  updateResource: (name, path, content) => postJSON(`/api/agents/skills/${enc(name)}/resources/${path}`, { content }),
  deleteResource: (name, path) => fetchJSON(`/api/agents/skills/${enc(name)}/resources/${path}`, { method: 'DELETE' }),
  listGitRepos: () => fetchJSON('/api/agents/git-repos'),
  addGitRepo: (url) => postJSON('/api/agents/git-repos', { url }),
  syncGitRepo: (id) => postJSON(`/api/agents/git-repos/${enc(id)}/sync`, {}),
  toggleGitRepo: (id) => postJSON(`/api/agents/git-repos/${enc(id)}/toggle`, {}),
  deleteGitRepo: (id) => fetchJSON(`/api/agents/git-repos/${enc(id)}`, { method: 'DELETE' }),
}

// Usage API
export const usageApi = {
  getMyUsage: (period) => fetchJSON(`/api/auth/usage?period=${period || 'month'}`),
  getAdminUsage: (period, userId) => {
    let url = `/api/auth/admin/usage?period=${period || 'month'}`

View on GitHub (pinned to 44413a9d06)

Solutions

  1. Check the status: 413 → increase proxy client_max_body_size / upload smaller archive; 401/403 → provide admin credentials; 400 → fix the archive structure (SKILL.md at root)
  2. Re-zip the skill so SKILL.md sits at the archive root and re-import
  3. Verify the endpoint exists: curl -i -F file=@skill.zip $BASE/api/agents/skills/import
  4. Improve the helper to surface the server's JSON error message (see exampleFix)

Example fix

// before
import: (file) => { const fd = new FormData(); fd.append('file', file); return fetch(apiUrl('/api/agents/skills/import'), { method: 'POST', body: fd }).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }); },

// after — surface the server's reason
import: (file) => { const fd = new FormData(); fd.append('file', file); return fetch(apiUrl('/api/agents/skills/import'), { method: 'POST', body: fd }).then(async r => { if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d?.error?.message || `HTTP ${r.status}`); } return r.json(); }); },
Defensive patterns

Strategy: validation

Validate before calling

// client-side pre-checks before upload
if (!(file instanceof File) || file.size === 0) throw new Error('select a non-empty archive')
if (!/\.zip$/i.test(file.name)) throw new Error('skill import expects a .zip archive')

Type guard

function isSkillArchive(file) {
  return file instanceof File && file.size > 0 && file.size < 50 * 1024 * 1024 && /\.zip$/i.test(file.name)
}

Try / catch

try { await skillsApi.import(file) } catch (e) {
  const status = e.message.match(/HTTP (\d+)/)?.[1]
  if (status === '413') addToast('archive too large', 'error')
  else if (status === '403') addToast('admin access required', 'error')
  else addToast(e.message, 'error')
}

Prevention

When it happens

Trigger: Uploading a skill bundle file that the server rejects: malformed zip, missing SKILL.md, skill name conflicts, file too large (413), or unauthenticated access to the admin endpoint (401/403). Also plain 404 when the agents/skills API is absent in the deployed version.

Common situations: Importing a hand-zipped skill directory that lacks the required manifest; importing into a LocalAI version predating the skills API; API-key protected instance; reverse proxy body-size limit (413) rejecting the archive.

Related errors


AI-assisted analysis of mudler/LocalAI@44413a9d06 (2026-08-15). Data as JSON: /api/errors/9a2f154b283d6cb9. Report an issue: GitHub.