odysseus-dev/odysseus · warning · SkillImportError

URL is required

Error message

URL is required

What it means

Raised at the top of parse_skill_source (services/memory/skill_importer.py) when the import URL, after strip(), is empty. It is the earliest, cheapest validation: no network or parsing happens before it, so hitting it means the caller passed '', None (str-coerced), or a whitespace-only string as the skill source URL.

Source

Thrown at services/memory/skill_importer.py:269

            timeout=timeout,
        ) as client:
            r = client.get(current, headers=headers)

        if r.status_code in (301, 302, 303, 307, 308):
            location = r.headers.get("location")
            if not location:
                return r
            current = urljoin(str(r.url), location)
            continue
        return r
    raise SkillImportError("too many redirects while fetching skill bundle")


def parse_skill_source(url: str) -> ResolvedSource:
    """Normalize skills.sh / GitHub web URLs into owner/repo/ref/path."""
    url = (url or "").strip()
    if not url:
        raise SkillImportError("URL is required")

    # ``urlparse`` only reports an unambiguous scheme when the URL carries the
    # ``scheme://`` form. Opaque schemes (``mailto:``, ``javascript:``) and a
    # schemeless ``host:port`` both parse a "scheme" that is not one, so they
    # fall through to the host check below and are rejected on the host instead.
    scheme = urlparse(url).scheme.lower()
    if scheme not in ("http", "https"):
        if scheme and url.lower().startswith(f"{scheme}://"):
            raise SkillImportError(f"unsupported URL scheme: {scheme}")
        # Schemeless "github.com/owner/repo" — accept only a supported host.
        rough_host = (urlparse("//" + url).hostname or "").lower()
        if rough_host not in _GITHUB_HOSTS and rough_host not in _SKILLS_SH_HOSTS:
            raise SkillImportError("Only GitHub or skills.sh URLs are supported")
        url = "https://" + url

    parsed = urlparse(url)
    hostname = (parsed.hostname or "").lower()
    if hostname not in _GITHUB_HOSTS and hostname not in _SKILLS_SH_HOSTS:

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check the caller: log the exact url value right before parse_skill_source to confirm it is empty.
  2. Fix the upstream source of the URL (form field, env var, config key) so a real GitHub/skills.sh URL is supplied.
  3. Add a required-field check in the UI/handler so users get a friendly 'URL is required' before the request reaches the parser.
  4. If the value is None by design in some path, guard that path before calling import.

Example fix

# before
url = request.form.get("url", "")   # unset field -> ""
parse_skill_source(url)
SkillImportError: URL is required

# after
url = (request.form.get("url") or "").strip()
if not url:
    return bad_request("Please paste a GitHub or skills.sh skill URL")
parse_skill_source(url)
Defensive patterns

Strategy: validation

Validate before calling

def validated_skill_url(raw: str | None) -> str:
    url = (raw or '').strip()
    if not url:
        raise ValueError('skill URL is required')
    return url

Type guard

def has_skill_url(raw) -> bool:
    return isinstance(raw, str) and bool(raw.strip())

Try / catch

from services.memory.skill_importer import SkillImportError, parse_skill_source

try:
    src = parse_skill_source(url or '')
except SkillImportError as e:
    if str(e) == 'URL is required':
        return bad_request('Please paste a skill URL.')
    raise

Prevention

When it happens

Trigger: Calling the skill-import API/handler with url omitted, an empty string, or a value that stringifies to blank; upstream UI passing an unset form field; a script reading a URL from an env var or clipboard that came back empty.

Common situations: Frontend bug submitting the import dialog before the URL field is filled; automation where the URL variable was never populated; copy-paste that grabbed only whitespace; None default leaking through from a calling layer.

Related errors


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