{"record":{"id":"212affe91f9dd1a2","repo":"odysseus-dev/odysseus","slug":"invalid-raw-github-url","errorCode":null,"errorMessage":"Invalid raw GitHub URL","messagePattern":"Invalid raw GitHub URL","errorType":"validation","errorClass":"SkillImportError","httpStatus":null,"severity":"error","filePath":"services/memory/skill_importer.py","lineNumber":319,"sourceCode":"            raise SkillImportError(\n                \"skills.sh did not redirect to GitHub — open the skill's \"\n                \"repository on GitHub, navigate to the exact skill folder or \"\n                \"SKILL.md file, and paste that URL; the repository-root link \"\n                \"alone is not sufficient\"\n            )\n        url = final\n\n    # Update parsed and hostname to reflect the new GitHub URL\n    parsed = urlparse(url)\n    hostname = (parsed.hostname or \"\").lower()\n\n    _assert_github_url(url)\n\n    if hostname == \"raw.githubusercontent.com\":\n        # /owner/repo/ref/path/to/file\n        bits = [p for p in parsed.path.split(\"/\") if p]\n        if len(bits) < 4:\n            raise SkillImportError(\"Invalid raw GitHub URL\")\n        owner, repo, ref = bits[0], bits[1], bits[2]\n        path = \"/\".join(bits[3:])\n        return ResolvedSource(owner=owner, repo=repo, ref=ref, path=path)\n\n    bits = [p for p in parsed.path.split(\"/\") if p]\n    if len(bits) < 2:\n        raise SkillImportError(\"Invalid GitHub URL\")\n    owner, repo = bits[0], bits[1]\n    ref = \"main\"\n    path = \"\"\n\n    if len(bits) >= 4 and bits[2] in (\"tree\", \"blob\"):\n        ref = bits[3]\n        path = \"/\".join(bits[4:])\n    elif len(bits) == 2:\n        path = \"\"\n    else:\n        raise SkillImportError(\"GitHub URL must include /tree/<branch>/... or /blob/<branch>/...\")","sourceCodeStart":301,"sourceCodeEnd":337,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/services/memory/skill_importer.py#L301-L337","documentation":"Raised by parse_skill_source (services/memory/skill_importer.py) when a raw.githubusercontent.com URL has fewer than 4 non-empty path segments. Raw URLs must follow /owner/repo/ref/path/to/file — the importer maps segments 1-3 to owner/repo/ref and the rest to the in-repo path, so a URL like https://raw.githubusercontent.com/owner/repo or .../owner/repo/main cannot identify a file and is rejected as malformed.","triggerScenarios":"Pasting https://raw.githubusercontent.com/<owner>/<repo> (repo root), .../<owner>/<repo>/<branch> (branch with no file), or a URL whose path collapsed to empty (trailing garbage after the host); hand-constructing a raw URL and forgetting the file component.","commonSituations":"Users trimming the URL while sharing; copying the raw base from docs examples and appending only owner/repo; branch names with slashes making the segment count look right but the intended file missing.","solutions":["Use a complete raw URL including ref and file path: https://raw.githubusercontent.com/<owner>/<repo>/<ref>/skills/<name>/SKILL.md.","Get the URL the reliable way: open the file on github.com, click the Raw button, copy the address bar.","If the branch contains slashes, that is fine — segment 3 onward is treated as ref+path only when counts line up; prefer the tree/blob web URL form to avoid ambiguity.","Verify the URL resolves: curl -I <raw url> should return 200."],"exampleFix":"# before\nimport_skill(\"https://raw.githubusercontent.com/owner/repo\")\nSkillImportError: Invalid raw GitHub URL\n\n# after\nimport_skill(\"https://raw.githubusercontent.com/owner/repo/main/skills/foo/SKILL.md\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef parse_raw_url(url: str) -> tuple[str, str, str, str]:\n    bits = [p for p in urlparse(url).path.split('/') if p]\n    if urlparse(url).hostname != 'raw.githubusercontent.com' or len(bits) < 4:\n        raise ValueError('raw URL must be https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>')\n    return bits[0], bits[1], bits[2], '/'.join(bits[3:])","typeGuard":"def is_valid_raw_github_url(url: str) -> bool:\n    try:\n        parse_raw_url(url)\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    src = parse_skill_source(raw_url)\nexcept SkillImportError as e:\n    if str(e) == 'Invalid raw GitHub URL':\n        return bad_request('Raw URLs need owner/repo/branch/file, e.g. .../raw.githubusercontent.com/o/r/main/SKILL.md')\n    raise","preventionTips":["Get raw URLs via the Raw button on github.com rather than typing them by hand.","Validate the 4-segment shape before submission.","curl -I the URL to confirm it returns 200 before import."],"tags":["url-validation","github","raw-content","skill-import"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}