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

  1. 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.
  2. Copy the destination URL from the last Location and paste the canonical https://github.com/.../tree/<ref>/<path> form into the importer.
  3. If the chain loops (same Location repeating), the linking site is broken — report it and import from GitHub directly.
  4. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/69c3d92f668f42bf. Report an issue: GitHub.