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
- 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)
- Re-zip the skill so SKILL.md sits at the archive root and re-import
- Verify the endpoint exists: curl -i -F file=@skill.zip $BASE/api/agents/skills/import
- 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
- Validate file presence, extension, and size before POSTing
- Include the API key header for admin endpoints
- Patch the helper to read the server's JSON error so users see the real reason
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
- Export failed
- HTTP ${response.status}
- HTTP ${response.status}
- HTTP ${response.status}
- status: HTTP ${statusRes.status}
AI-assisted analysis of mudler/LocalAI@44413a9d06 (2026-08-15).
Data as JSON: /api/errors/9a2f154b283d6cb9.
Report an issue: GitHub.