{"record":{"id":"65d415aaa8d27be0","repo":"odysseus-dev/odysseus","slug":"url-is-required-65d415","errorCode":null,"errorMessage":"URL is required","messagePattern":"URL is required","errorType":"validation","errorClass":"SkillImportError","httpStatus":null,"severity":"warning","filePath":"services/memory/skill_importer.py","lineNumber":269,"sourceCode":"            timeout=timeout,\n        ) as client:\n            r = client.get(current, headers=headers)\n\n        if r.status_code in (301, 302, 303, 307, 308):\n            location = r.headers.get(\"location\")\n            if not location:\n                return r\n            current = urljoin(str(r.url), location)\n            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:","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/services/memory/skill_importer.py#L251-L287","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the caller: log the exact url value right before parse_skill_source to confirm it is empty.","Fix the upstream source of the URL (form field, env var, config key) so a real GitHub/skills.sh URL is supplied.","Add a required-field check in the UI/handler so users get a friendly 'URL is required' before the request reaches the parser.","If the value is None by design in some path, guard that path before calling import."],"exampleFix":"# before\nurl = request.form.get(\"url\", \"\")   # unset field -> \"\"\nparse_skill_source(url)\nSkillImportError: URL is required\n\n# after\nurl = (request.form.get(\"url\") or \"\").strip()\nif not url:\n    return bad_request(\"Please paste a GitHub or skills.sh skill URL\")\nparse_skill_source(url)","handlingStrategy":"validation","validationCode":"def validated_skill_url(raw: str | None) -> str:\n    url = (raw or '').strip()\n    if not url:\n        raise ValueError('skill URL is required')\n    return url","typeGuard":"def has_skill_url(raw) -> bool:\n    return isinstance(raw, str) and bool(raw.strip())","tryCatchPattern":"from services.memory.skill_importer import SkillImportError, parse_skill_source\n\ntry:\n    src = parse_skill_source(url or '')\nexcept SkillImportError as e:\n    if str(e) == 'URL is required':\n        return bad_request('Please paste a skill URL.')\n    raise","preventionTips":["Make the URL field required in the UI and disable submit while empty.","strip() user input before every parse call.","Distinguish empty-input errors from format errors in handlers so users get actionable messages."],"tags":["validation","user-input","skill-import","python"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}