{"record":{"id":"3ce01417a07e9276","repo":"odysseus-dev/odysseus","slug":"d-d-detail-d-detail-http-res-status","errorCode":null,"errorMessage":"d && d.detail ? d.detail : ('HTTP ' + res.status)","messagePattern":"d && d\\.detail \\? d\\.detail : \\('HTTP ' \\+ res\\.status\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/visual_report.py","lineNumber":1090,"sourceCode":"    }}, {{ rootMargin: '-10% 0px -75% 0px', threshold: 0 }});\n    headings.forEach(function(h) {{ io.observe(h); }});\n  }}\n\n  // Chat about this research — POST to spinoff and redirect to the new chat\n  var chatBtn = document.getElementById('btn-chat-about');\n  if (chatBtn) {{\n    chatBtn.addEventListener('click', function() {{\n      var researchId = chatBtn.dataset.researchId;\n      if (!researchId) return;\n      var origLabel = chatBtn.innerHTML;\n      chatBtn.disabled = true;\n      chatBtn.innerHTML = '<span>Creating chat…</span>';\n      fetch('/api/research/spinoff/' + encodeURIComponent(researchId), {{\n        method: 'POST', credentials: 'same-origin',\n      }}).then(function(res) {{\n        if (!res.ok) {{\n          return res.json().then(function(d) {{\n            throw new Error(d && d.detail ? d.detail : ('HTTP ' + res.status));\n          }}, function() {{ throw new Error('HTTP ' + res.status); }});\n        }}\n        return res.json();\n      }}).then(function(data) {{\n        if (!data || !data.session_id) {{\n          throw new Error('Server did not return a session id');\n        }}\n        var url = '/#' + data.session_id;\n        var opened = false;\n        // The report typically opens in a new tab — if we have access to the\n        // original Odysseus tab, navigate it and close this report tab so the\n        // user lands directly in the new chat.\n        try {{\n          if (window.opener && !window.opener.closed) {{\n            window.opener.location.href = url;\n            window.opener.location.reload();\n            window.opener.focus();\n            opened = true;","sourceCodeStart":1072,"sourceCodeEnd":1108,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/src/visual_report.py#L1072-L1108","documentation":"Raised by parse_skill_source when a github.com URL path has 3 segments (owner/repo/<word>) or its third segment is not 'tree' or 'blob'. The parser only accepts repo-root URLs (2 segments), or full /tree/<ref>/... or /blob/<ref>/... URLs; anything else (e.g. /releases/tag/v1, /raw/..., /commits/main) falls into the else branch. ref defaults to 'main', so a bare repo URL assumes the default branch is main.","triggerScenarios":"Calling fetch_skill_bundle()/parse_skill_source with 'https://github.com/owner/repo/releases/tag/v1.0' (bits[2]='releases'), 'https://github.com/owner/repo/blob' (len(bits)==3, no ref segment), or 'https://github.com/owner/repo/commit/abc123'. Only len(bits)==2, or len(bits)>=4 with bits[2] in ('tree','blob'), parse successfully.","commonSituations":"User pastes a link copied from the GitHub releases page, a commits page, or a compare URL instead of navigating to the folder/file view. Also happens when someone hand-trims a URL and drops the branch segment, leaving /blob with nothing after it.","solutions":["Use a URL of the form https://github.com/<owner>/<repo>/tree/<branch>/<path> (folder) or https://github.com/<owner>/<repo>/blob/<branch>/<path> (SKILL.md file)","Or use the plain repo root https://github.com/<owner>/<repo> — this parses with ref='main' and path=''","If linking a specific commit or tag, wrap it in tree/ form: .../tree/<tag-or-sha>/<path>","Verify the branch name in the URL actually exists (master vs main only matters later, at fetch time, since bare URLs default to main)"],"exampleFix":"// before\nurl = \"https://github.com/acme/awesome-skills/releases/tag/v2\"\nbundle = fetch_skill_bundle(url)  # SkillImportError\n\n// after\nurl = \"https://github.com/acme/awesome-skills/tree/v2/skills/debug-helper\"\nbundle = fetch_skill_bundle(url)","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\n_RE = re.compile(r\"^https?://(?:www\\.)?github\\.com/[^/]+/[^/]+(?:/(?:tree|blob)/[^/]+(?:/.+)?)?/?$\")\n\ndef is_valid_skill_github_url(url: str) -> bool:\n    return bool(_RE.match(url.strip()))","typeGuard":"def is_parseable_skill_source(url: str) -> bool:\n    \"\"\"True when parse_skill_source will not raise on URL shape.\"\"\"\n    try:\n        bits = [p for p in urlparse(url).path.split(\"/\") if p]\n    except Exception:\n        return False\n    return len(bits) == 2 or (len(bits) >= 4 and bits[2] in (\"tree\", \"blob\"))","tryCatchPattern":"from services.memory.skill_importer import SkillImportError, fetch_skill_bundle\n\ntry:\n    files, src = fetch_skill_bundle(url)\nexcept SkillImportError as e:\n    if \"must include /tree\" in str(e):\n        raise UserInputError(f\"Re-link {url!r} as .../tree/<branch>/<path> or .../blob/<branch>/<path>\") from e\n    raise","preventionTips":["In UI, build GitHub links from the address bar while viewing the folder or SKILL.md file, never from releases/commits pages","Normalize user-pasted URLs through is_parseable_skill_source before calling the importer","Offer a URL picker that only emits tree/ or blob/ forms"],"tags":["github","url-parsing","validation","skill-import"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}