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

  1. 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
  2. For 'already exists', delete the existing skill via the delete endpoint or pick a different name in the frontmatter/filename
  3. For ZIP errors, open the archive locally and confirm it contains a SKILL.md at or near the root
  4. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/8635b05c8be2defe. Report an issue: GitHub.