{"record":{"id":"8c4f78ef8c259b05","repo":"odysseus-dev/odysseus","slug":"unsupported-url-scheme-scheme","errorCode":null,"errorMessage":"unsupported URL scheme: {scheme}","messagePattern":"unsupported URL scheme: (.+?)","errorType":"validation","errorClass":"SkillImportError","httpStatus":null,"severity":"error","filePath":"services/memory/skill_importer.py","lineNumber":278,"sourceCode":"            continue\n        return r\n    raise SkillImportError(\"too many redirects while fetching skill bundle\")\n\n\ndef parse_skill_source(url: str) -> ResolvedSource:\n    \"\"\"Normalize skills.sh / GitHub web URLs into owner/repo/ref/path.\"\"\"\n    url = (url or \"\").strip()\n    if not url:\n        raise SkillImportError(\"URL is required\")\n\n    # ``urlparse`` only reports an unambiguous scheme when the URL carries the\n    # ``scheme://`` form. Opaque schemes (``mailto:``, ``javascript:``) and a\n    # schemeless ``host:port`` both parse a \"scheme\" that is not one, so they\n    # fall through to the host check below and are rejected on the host instead.\n    scheme = urlparse(url).scheme.lower()\n    if scheme not in (\"http\", \"https\"):\n        if scheme and url.lower().startswith(f\"{scheme}://\"):\n            raise SkillImportError(f\"unsupported URL scheme: {scheme}\")\n        # Schemeless \"github.com/owner/repo\" — accept only a supported host.\n        rough_host = (urlparse(\"//\" + url).hostname or \"\").lower()\n        if rough_host not in _GITHUB_HOSTS and rough_host not in _SKILLS_SH_HOSTS:\n            raise SkillImportError(\"Only GitHub or skills.sh URLs are supported\")\n        url = \"https://\" + url\n\n    parsed = urlparse(url)\n    hostname = (parsed.hostname or \"\").lower()\n    if hostname not in _GITHUB_HOSTS and hostname not in _SKILLS_SH_HOSTS:\n        raise SkillImportError(\"Only GitHub or skills.sh URLs are supported\")\n\n    # A skills.sh link is only usable if it redirects to an exact supported\n    # GitHub host. Scraping the page body for a github.com link cannot work:\n    # skill pages only ever link the repository root, never the skill's\n    # subdirectory, so the scrape resolves every skill in a repo to the same\n    # (wrong) bundle. Fail with an actionable message instead.\n    if hostname in _SKILLS_SH_HOSTS:\n        r = _get_checked(url, timeout=20.0)","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/services/memory/skill_importer.py#L260-L296","documentation":"Raised by parse_skill_source (services/memory/skill_importer.py) when the URL carries an explicit scheme:// form whose scheme is neither http nor https — e.g. file://, ftp://, ssh://, javascript:, or a non-web scheme. The guard distinguishes real schemes from urlparse artifacts (schemeless 'host:port' or opaque schemes), and only the true scheme:// shape raises this specific error; unsupported hosts in other shapes get the host-based message instead.","triggerScenarios":"Importing a skill from file:///Users/me/skills/foo; git@github.com:owner/repo.git style SSH URLs misparsed as a scheme; ftp:// or http+insecure:// URLs; users pasting a javascript: or data: string; any pasted URL with a typo'd scheme like htps:// that urlparse still treats as a scheme when followed by //.","commonSituations":"Users pasting the SSH clone URL instead of the HTTPS web URL; trying to import a locally cloned skill via file://; CI passing a git-remote-style URL where a browser URL is expected.","solutions":["Use an https:// GitHub web URL: https://github.com/<owner>/<repo>/tree/<ref>/<skill-path>.","Convert SSH remotes to HTTPS: git@github.com:owner/repo.git → https://github.com/owner/repo.","For a local skill, do not use file:// — push it to a GitHub repo first or copy it into the skills directory manually if your tooling allows.","Fix typo'd schemes (htps:// → https://)."],"exampleFix":"# before\nimport_skill(\"file:///Users/me/skills/foo\")\nSkillImportError: unsupported URL scheme: file\n\n# after\nimport_skill(\"https://github.com/me/repo/tree/main/skills/foo\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef assert_http_url(url: str) -> None:\n    scheme = urlparse(url.strip()).scheme.lower()\n    if scheme not in ('http', 'https', ''):  # '' = schemeless host/path, handled by host check\n        raise ValueError(f'unsupported scheme {scheme!r}; use https://github.com/...')","typeGuard":"def is_http_or_schemeless(url: str) -> bool:\n    return urlparse(url.strip()).scheme.lower() in ('http', 'https', '')","tryCatchPattern":"try:\n    src = parse_skill_source(url)\nexcept SkillImportError as e:\n    if 'unsupported URL scheme' in str(e):\n        # convert common SSH/remote forms to https\n        url = url.replace('git@github.com:', 'https://github.com/').removesuffix('.git')\n        src = parse_skill_source(url)\n    else:\n        raise","preventionTips":["Convert git remotes to HTTPS before feeding them to web-URL parsers (git remote get-url origin + string replace).","Reject file:// and other schemes client-side with a hint about the accepted format.","Document the accepted URL forms next to the import input field."],"tags":["url-validation","security","user-input","skill-import"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}