odysseus-dev/odysseus · error · SkillImportError
too many redirects while fetching skill bundle
Error message
too many redirects while fetching skill bundle
What it means
Raised by the manual redirect loop in _get_checked (services/memory/skill_importer.py) after _MAX_FETCH_REDIRECTS (5) hops: each 301/302/303/307/308 response with a Location header advances 'current' and every hop is re-validated (SSRF check per hop), and exceeding the budget aborts with this error. A redirect without Location is returned as the final response instead of erroring. The cap prevents both redirect loops and validation-bypass chains.
Source
Thrown at services/memory/skill_importer.py:262
"""
current = url
for _ in range(_MAX_FETCH_REDIRECTS + 1):
pinned_ips = _resolve_and_check_url(current)
with httpx.Client(
transport=_PinnedTransport(pinned_ips),
follow_redirects=False,
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()View on GitHub (pinned to f9235ebbf1)
Solutions
- Trace the chain manually to count hops: curl -sIL <url> | grep -i '^location' — if it exceeds 5 before landing on github.com, use the final GitHub URL directly for import.
- Copy the destination URL from the last Location and paste the canonical https://github.com/.../tree/<ref>/<path> form into the importer.
- If the chain loops (same Location repeating), the linking site is broken — report it and import from GitHub directly.
- Only raise _MAX_FETCH_REDIRECTS if you have audited why legitimate chains are longer; the cap is a safety bound.
Example fix
# before
import_skill("https://skills.sh/owner/repo/skill")
SkillImportError: too many redirects while fetching skill bundle
# after
$ curl -sIL https://skills.sh/owner/repo/skill | grep -i '^location' | tail -1
https://github.com/owner/repo/tree/main/skills/skill
>>> import_skill("https://github.com/owner/repo/tree/main/skills/skill") Defensive patterns
Strategy: validation
Validate before calling
import httpx
def final_url_after_redirects(url: str, max_hops: int = 5) -> str | None:
seen = 0
with httpx.Client(follow_redirects=False, timeout=20) as c:
while seen < max_hops:
r = c.get(url)
if r.status_code not in (301, 302, 303, 307, 308):
return str(r.url)
loc = r.headers.get('location')
if not loc:
return str(r.url)
url = str(httpx.URL(url).join(loc))
seen += 1
return None # exceeds budget — do not fetch Type guard
def redirect_chain_within_budget(url: str, max_hops: int = 5) -> bool:
return final_url_after_redirects(url, max_hops) is not None Try / catch
from services.memory.skill_importer import SkillImportError
try:
import_skill(url)
except SkillImportError as e:
if 'too many redirects' in str(e):
# resolve manually and import the destination directly
dest = final_url_after_redirects(url, max_hops=10)
import_skill(dest) # dest must be a github.com URL
else:
raise Prevention
- Prefer canonical github.com URLs over skills.sh/shortened links to sidestep redirect chains entirely.
- Keep a bounded hop limit in any redirect-following fetcher you write; loops otherwise hang forever.
- Trace chains with curl -sIL before automating imports of new link sources.
When it happens
Trigger: A skills.sh URL whose redirect chain is longer than 5 hops (tracker → CDN → github.com); a redirect loop (A→B→A) that would otherwise spin forever; skills.sh or an intermediary adding sequential analytics redirects; chains that never reach a terminal 2xx within the budget.
Common situations: skills.sh changing its redirect architecture and inserting extra hops; URL shorteners stacked on shorteners in shared skill links; a misconfigured repo redirect on GitHub's side chaining through several moved repos.
Related errors
- outbound URL blocked: {reason}
- outbound URL blocked: host did not resolve to a usable addre
- {context} must stay on GitHub (got {host or 'unknown host'})
- unsafe path: {rel!r}
- unsupported URL scheme: {scheme}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/69c3d92f668f42bf.
Report an issue: GitHub.