{"record":{"id":"8635b05c8be2defe","repo":"sipeed/picoclaw","slug":"err-error-dynamic-from-importuploadedskill","errorCode":null,"errorMessage":"err.Error() [dynamic from importUploadedSkill]","messagePattern":"err\\.Error\\(\\) \\[dynamic from importUploadedSkill\\]","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/backend/api/skills.go","lineNumber":476,"sourceCode":"\t\treturn\n\t}\n\tdefer uploadedFile.Close()\n\n\tcontent, err := io.ReadAll(io.LimitReader(uploadedFile, maxImportedSkillSize+1))\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to read file: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\tif len(content) > maxImportedSkillSize {\n\t\thttp.Error(w, \"file exceeds 1MB limit\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tworkspaceSkillWriteMu.Lock()\n\tdefer workspaceSkillWriteMu.Unlock()\n\n\timportedSkill, statusCode, err := importUploadedSkill(cfg, fileHeader.Filename, content)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), statusCode)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tjson.NewEncoder(w).Encode(importedSkill)\n}\n\nfunc (h *Handler) handleDeleteSkill(w http.ResponseWriter, r *http.Request) {\n\tcfg, err := config.LoadConfig(h.configPath)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Failed to load config: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tloader := newSkillsLoader(cfg.WorkspacePath())\n\tname := r.PathValue(\"name\")\n\tworkspaceSkillWriteMu.Lock()\n\tdefer workspaceSkillWriteMu.Unlock()","sourceCodeStart":458,"sourceCodeEnd":494,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/web/backend/api/skills.go#L458-L494","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# distinguish outcomes by status, not just body\ncurl -s -o /dev/null -w '%{http_code}' -F 'file=@skill.zip' /api/skills/import\n# 409 -> delete existing; 400 -> fix archive; 500 -> check server disk/perms","handlingStrategy":"try-catch","validationCode":"// client: pre-flight the two common causes\n// 1) name collision: GET /skills and compare derived names\n// 2) archive sanity: ensure the ZIP contains a SKILL.md entry before upload\nconst entries = await zipEntries(file)\nif (!entries.some(e => e.name === 'SKILL.md' || e.name.endsWith('/SKILL.md'))) throw new Error('not a skill archive')","typeGuard":"// Go: classify by status\nclassify := func(code int) string {\n  switch {\n  case code == 409: return \"conflict\"\n  case code >= 400 && code < 500: return \"bad-upload\"\n  default: return \"server-error\"\n  }\n}","tryCatchPattern":"// branch on the status that importUploadedSkill chose\nres, err := client.Post(url, multipartBody)\nif err == nil && !res.OK {\n  msg, _ := io.ReadAll(res.Body)\n  switch { case res.StatusCode == 409: /* delete-or-rename flow */\n           case res.StatusCode < 500: /* fix payload, show msg */\n           default: /* surface msg to ops, no blind retry */ }\n}","preventionTips":["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"],"tags":["go","http","dynamic-error","import","zip","validation","skills"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}