ComposioHQ/composio · error · BlockedInternalUrlError

Refusing to fetch: too many redirects (max {max_redirects})

Error message

Refusing to fetch: too many redirects (max {max_redirects})

What it means

safe_request follows HTTP redirects manually (re-validating each hop) and enforces a hard cap. When the number of redirect hops exceeds max_redirects, it raises BlockedInternalUrlError instead of continuing, since each hop must be re-validated for SSRF safety.

Source

Thrown at python/composio/utils/url_safety.py:188

    current_url = url

    for _ in range(max_redirects + 1):
        response = _pinned_request(method, current_url, **kwargs)

        location = response.headers.get("Location")
        if response.status_code not in _REDIRECT_STATUS_CODES or location is None:
            return response

        response.close()
        current_url = urljoin(current_url, location)

        # `requests` rewinds the body itself when it follows a redirect; doing
        # it manually means doing that too, or a retried upload sends nothing.
        seek = getattr(body, "seek", None)
        if callable(seek):
            seek(0)

    raise BlockedInternalUrlError(
        f"Refusing to fetch: too many redirects (max {max_redirects})"
    )


class _PinnedAddressAdapter(requests.adapters.HTTPAdapter):
    """Transport adapter that connects to a pre-validated address.

    The hostname is left untouched on the connection, so the ``Host`` header
    and the TLS SNI/certificate check still use it; only the address the
    socket dials is replaced. Doing it the other way round — rewriting
    ``conn._dns_host`` for the whole connection — would also rewrite
    ``conn.host``, which urllib3 derives from it, and the request would go out
    with an IP in ``Host`` and an IP in SNI, failing certificate verification
    against every real origin.

    This reaches into two urllib3 internals, ``HTTPConnection._new_conn`` and
    ``HTTPConnection._dns_host``. ``test_url_safety_pinning.py`` asserts both
    exist so a urllib3 upgrade that removes them fails loudly rather than

View on GitHub (pinned to 64b1b85502)

Solutions

  1. curl -IL <url> to inspect the redirect chain and find the loop
  2. Fix the server-side redirect loop (trailing slashes, http/https canonicalization)
  3. Request the final URL directly, bypassing the chain
  4. Pass a higher max_redirects to safe_request if the chain is legitimately long

Example fix

# before
safe_request('GET', url)  # long auth redirect chain
# after
safe_request('GET', final_url_after_auth)  # or safe_request('GET', url, max_redirects=20)
Defensive patterns

Strategy: retry

Validate before calling

import requests
r = requests.head(url, allow_redirects=False)
# follow manually up to N hops to measure chain length before calling safe_request

Try / catch

try:
    safe_request('GET', url)
except BlockedInternalUrlError as e:
    if 'too many redirects' in str(e):
        safe_request('GET', final_url)  # resolve chain, or raise max_redirects

Prevention

When it happens

Trigger: Fetching a URL whose redirect chain is longer than max_redirects (default typically 10) — e.g. long SSO/auth chains, redirect loops (A→B→A), or misconfigured servers bouncing between trailing-slash and non-slash variants.

Common situations: Auth redirect loops after session expiry, CDN misconfiguration, presigned upload URLs that bounce through multiple regions, redirect loops between http and https.

Related errors


AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28). Data as JSON: /api/errors/80944186f02d5b17. Report an issue: GitHub.