sipeed/picoclaw · error
err.Error() [dynamic from importUploadedSkill]
Error message
err.Error() [dynamic from importUploadedSkill]
What it means
Not one message but the pass-through of err.Error() with a companion statusCode from importUploadedSkill (web/backend/api/skills.go:474-477; implementation at 842-928). Depending on the upload, the error can be: errImportedSkillExists (skill directory already exists), an invalid skill name from normalizeImportedSkillName (400), 'invalid ZIP archive' from ExtractZipFile (400), 'failed to read SKILL.md from archive' (400), findImportedSkillRoot failures (400), or 500-class 'Failed to create temp directory' / 'Failed to save skill' / 'Failed to normalize skill'. The HTTP status travels alongside, so 400/409/500 semantics must be read from the status line.
Source
Thrown at web/backend/api/skills.go:476
return
}
defer uploadedFile.Close()
content, err := io.ReadAll(io.LimitReader(uploadedFile, maxImportedSkillSize+1))
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read file: %v", err), http.StatusBadRequest)
return
}
if len(content) > maxImportedSkillSize {
http.Error(w, "file exceeds 1MB limit", http.StatusBadRequest)
return
}
workspaceSkillWriteMu.Lock()
defer workspaceSkillWriteMu.Unlock()
importedSkill, statusCode, err := importUploadedSkill(cfg, fileHeader.Filename, content)
if err != nil {
http.Error(w, err.Error(), statusCode)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(importedSkill)
}
func (h *Handler) handleDeleteSkill(w http.ResponseWriter, r *http.Request) {
cfg, err := config.LoadConfig(h.configPath)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
return
}
loader := newSkillsLoader(cfg.WorkspacePath())
name := r.PathValue("name")
workspaceSkillWriteMu.Lock()
defer workspaceSkillWriteMu.Unlock()View on GitHub (pinned to 49183d7e8d)
Solutions
- Read the HTTP status: 409 means name collision (delete or rename first), 400 means payload problems (fix name/archive), 500 means server-side filesystem trouble
- For 'already exists', delete the existing skill via the delete endpoint or pick a different name in the frontmatter/filename
- For ZIP errors, open the archive locally and confirm it contains a SKILL.md at or near the root
- For 500s, apply the filesystem checks: space, permissions, concurrent access to <workspace>/skills
Example fix
# distinguish outcomes by status, not just body
curl -s -o /dev/null -w '%{http_code}' -F 'file=@skill.zip' /api/skills/import
# 409 -> delete existing; 400 -> fix archive; 500 -> check server disk/perms Defensive patterns
Strategy: try-catch
Validate before calling
// client: pre-flight the two common causes
// 1) name collision: GET /skills and compare derived names
// 2) archive sanity: ensure the ZIP contains a SKILL.md entry before upload
const entries = await zipEntries(file)
if (!entries.some(e => e.name === 'SKILL.md' || e.name.endsWith('/SKILL.md'))) throw new Error('not a skill archive') Type guard
// Go: classify by status
classify := func(code int) string {
switch {
case code == 409: return "conflict"
case code >= 400 && code < 500: return "bad-upload"
default: return "server-error"
}
} Try / catch
// branch on the status that importUploadedSkill chose
res, err := client.Post(url, multipartBody)
if err == nil && !res.OK {
msg, _ := io.ReadAll(res.Body)
switch { case res.StatusCode == 409: /* delete-or-rename flow */
case res.StatusCode < 500: /* fix payload, show msg */
default: /* surface msg to ops, no blind retry */ }
} Prevention
- Validate filename/frontmatter-derived names against the existing skill list before import
- Test archives locally (unzip + look for SKILL.md) before uploading
- Log status+body pairs client-side; the message text is the only diagnostic the server returns
When it happens
Trigger: Any POST to the import endpoint whose payload fails one of the validation/persistence steps: name collisions, non-skill ZIPs, ZIPs without SKILL.md anywhere, unwritable workspace, or disk-full during copyImportedSkillTree.
Common situations: Re-importing a skill that already exists under the same derived name; uploading a project ZIP that is not a skill; uploads whose filename/frontmatter produce a rejected name (bad characters, reserved names); server filesystem issues surfacing as 500 through this same path.
Related errors
- build request: %w
- skill frontmatter name %q does not match target skill %q
- skill frontmatter description is required
- skill patch frontmatter name %q does not match target skill
- resolve lifecycle delete skill name: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/8635b05c8be2defe.
Report an issue: GitHub.