{"record":{"id":"bd916ea24574f97e","repo":"odysseus-dev/odysseus","slug":"skill-import-failed","errorCode":null,"errorMessage":"Skill import failed","messagePattern":"Skill import failed","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/skills_routes.py","lineNumber":1280,"sourceCode":"            fetch_skill_bundle,\n        )\n\n        try:\n            files, _src = fetch_skill_bundle(body.url.strip())\n            entry = skills_manager.import_bundle_from_files(\n                files,\n                owner=user,\n                source_url=body.url.strip(),\n            )\n        except SkillImportError as e:\n            raise HTTPException(400, str(e)) from e\n        except httpx.HTTPError as e:\n            logger.warning(\"skill import fetch failed: %s\", e)\n            detail = str(e).strip() or \"Could not download skill from URL\"\n            raise HTTPException(502, detail) from e\n        except Exception as e:\n            logger.error(\"skill import failed: %s\", e)\n            raise HTTPException(500, \"Skill import failed\") from e\n\n        _fire_skill_added(user)\n        return {\"ok\": True, \"skill\": entry, \"files\": len(files)}\n\n    @router.post(\"/add\")\n    async def add_skill(request: Request, body: SkillAddRequest):\n        user = _owner(request)\n        entry = skills_manager.add_skill(\n            # New shape\n            name=body.name,\n            description=body.description,\n            category=body.category,\n            tags=body.tags,\n            platforms=body.platforms,\n            requires_toolsets=body.requires_toolsets,\n            fallback_for_toolsets=body.fallback_for_toolsets,\n            when_to_use=body.when_to_use,\n            procedure=body.procedure,","sourceCodeStart":1262,"sourceCodeEnd":1298,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/skills_routes.py#L1262-L1298","documentation":"Generic 500 raised by the catch-all handler in the POST skill-import endpoint after SkillImportError (400) and httpx.HTTPError (502) were already handled. It means the import failed in a way the endpoint did not anticipate: not a validation/refusal (SkillImportError) and not a network fetch failure (httpx), but something during unpacking, filesystem writes, or skill registration. The original exception is logged server-side ('skill import failed: %s') and chained via 'from e'.","triggerScenarios":"POST to the skill import endpoint with a valid, downloadable URL whose payload is malformed on disk afterwards: a zip that downloads fine but fails to extract (corrupt archive, zip-slip path blocked, unreadable entries), a permissions or disk-full error while writing the skill directory, or an unexpected bug inside skills_manager.import_from_* (e.g. a None or KeyError while normalizing the imported skill's metadata).","commonSituations":"Importing a skill from a URL that returns an HTML error page with 200 status (parse/explosion downstream), importing archives built with unusual tooling (entries with odd permissions, symlinks), read-only or full skills directory, or a version mismatch after the import code changed its return shape while the route still expects (entry, files).","solutions":["Read the server log line 'skill import failed: <e>' — the chained exception names the real cause; the 500 itself is intentionally opaque.","If the cause is archive-related, re-zip the skill with only the SKILL.md directory contents (no symlinks, no absolute paths) and retry the import.","Check filesystem health: free space and write permission on the skills directory for the server process user.","If the cause is a metadata/None error, verify the source skill has a well-formed SKILL.md with required front-matter (name, description) before importing.","If it reproduces on a valid archive, report as a bug — the endpoint's return-shape contract with skills_manager may have drifted; capture the traceback from the logs."],"exampleFix":"# before\nexcept Exception as e:\n    logger.error(\"skill import failed: %s\", e)\n    raise HTTPException(500, \"Skill import failed\") from e\n\n# after (include exception class in detail for triage)\nexcept Exception as e:\n    logger.exception(\"skill import failed\")\n    raise HTTPException(500, f\"Skill import failed: {type(e).__name__}\") from e","handlingStrategy":"try-catch","validationCode":"# pre-flight: URL reachable and payload looks like a skill archive\nimport httpx\nr = httpx.head(url, follow_redirects=True, timeout=10)\nassert r.status_code == 200, f\"source unreachable: {r.status_code}\"\nctype = r.headers.get(\"content-type\", \"\")\nassert \"zip\" in ctype or \"octet-stream\" in ctype or \"text/plain\" in ctype, f\"unexpected payload: {ctype}\"","typeGuard":null,"tryCatchPattern":"try:\n    resp = client.post(f\"{base}/api/skills/import\", json={\"url\": url})\n    resp.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    code = e.response.status_code\n    if code == 502:\n        retry_with_mirror_or_report(\"download failed\", url)\n    elif code == 400:\n        fix_archive_per_detail(e.response.json()[\"detail\"])\n    elif code == 500:\n        check_server_logs(); repackage_skill_archive_and_retry_once(url)","preventionTips":["Validate the source URL responds 200 with a plausible content-type before importing.","Zip skills with relative paths only — no symlinks, no absolute entries.","Keep the skills directory writable and monitored for free space.","Run imports serially per owner to avoid partial-write collisions."],"tags":["skills","import","http-500","fastapi","filesystem"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}