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

  1. Read the server log line 'skill import failed: <e>' — the chained exception names the real cause; the 500 itself is intentionally opaque.
  2. 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.
  3. Check filesystem health: free space and write permission on the skills directory for the server process user.
  4. 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.
  5. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/bd916ea24574f97e. Report an issue: GitHub.