odysseus-dev/odysseus · error · HTTPException
Skill import failed
Error message
Skill import failed
What it means
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'.
Source
Thrown at routes/skills_routes.py:1280
fetch_skill_bundle,
)
try:
files, _src = fetch_skill_bundle(body.url.strip())
entry = skills_manager.import_bundle_from_files(
files,
owner=user,
source_url=body.url.strip(),
)
except SkillImportError as e:
raise HTTPException(400, str(e)) from e
except httpx.HTTPError as e:
logger.warning("skill import fetch failed: %s", e)
detail = str(e).strip() or "Could not download skill from URL"
raise HTTPException(502, detail) from e
except Exception as e:
logger.error("skill import failed: %s", e)
raise HTTPException(500, "Skill import failed") from e
_fire_skill_added(user)
return {"ok": True, "skill": entry, "files": len(files)}
@router.post("/add")
async def add_skill(request: Request, body: SkillAddRequest):
user = _owner(request)
entry = skills_manager.add_skill(
# New shape
name=body.name,
description=body.description,
category=body.category,
tags=body.tags,
platforms=body.platforms,
requires_toolsets=body.requires_toolsets,
fallback_for_toolsets=body.fallback_for_toolsets,
when_to_use=body.when_to_use,
procedure=body.procedure,View on GitHub (pinned to f9235ebbf1)
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.
Example fix
# before
except Exception as e:
logger.error("skill import failed: %s", e)
raise HTTPException(500, "Skill import failed") from e
# after (include exception class in detail for triage)
except Exception as e:
logger.exception("skill import failed")
raise HTTPException(500, f"Skill import failed: {type(e).__name__}") from e Defensive patterns
Strategy: try-catch
Validate before calling
# pre-flight: URL reachable and payload looks like a skill archive
import httpx
r = httpx.head(url, follow_redirects=True, timeout=10)
assert r.status_code == 200, f"source unreachable: {r.status_code}"
ctype = r.headers.get("content-type", "")
assert "zip" in ctype or "octet-stream" in ctype or "text/plain" in ctype, f"unexpected payload: {ctype}" Try / catch
try:
resp = client.post(f"{base}/api/skills/import", json={"url": url})
resp.raise_for_status()
except httpx.HTTPStatusError as e:
code = e.response.status_code
if code == 502:
retry_with_mirror_or_report("download failed", url)
elif code == 400:
fix_archive_per_detail(e.response.json()["detail"])
elif code == 500:
check_server_logs(); repackage_skill_archive_and_retry_once(url) Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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).
Related errors
- HTTP ${res.status}
- Failed to delete calendar
- Failed to list calendars
- Failed to list events
- Failed to create event
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/bd916ea24574f97e.
Report an issue: GitHub.