BerriAI/litellm · error · SSRFError

Redirect response has no Location header

Error message

Redirect response has no Location header

What it means

Raised by _extract_redirect_url during litellm's SSRF-protected fetch when a response is classified as a redirect (response.is_redirect) but its Location header is missing, empty, or not a string. RFC-compliant 3xx responses must carry Location, so this indicates a malformed/broken server or an HTTP client edge case, and litellm fails closed rather than guessing the next hop.

Source

Thrown at litellm/litellm_core_utils/url_utils.py:370

    expected_host: Final = _normalize_host(expected.hostname or "")
    if not candidate_host or candidate_host != expected_host:
        raise SSRFError("Origin mismatch on host")

    default_port: Final = 443 if candidate.scheme == "https" else 80
    candidate_port: Final = candidate.port if candidate.port is not None else default_port
    expected_port: Final = expected.port if expected.port is not None else default_port
    if candidate_port != expected_port:
        raise SSRFError("Origin mismatch on port")


_MAX_REDIRECTS: Final = 10


def _extract_redirect_url(response: Any, request_url: str) -> str:
    """Extract and resolve the redirect target from a response's Location header."""
    location: Final = response.headers.get("location")
    if not isinstance(location, str) or not location:
        raise SSRFError("Redirect response has no Location header")
    # Resolve relative URLs against the request URL
    return str(httpx.URL(request_url).join(location))


def safe_get(client: Any, url: str, **kwargs: Any) -> Any:
    """
    Fetch a user-supplied URL with SSRF protection on every redirect hop.

    Validates the initial URL and each redirect target before making the
    request. No DNS rebinding (resolve-and-rewrite). No redirect bypass
    (each hop validated). No breaking change for legitimate CDN redirects.

    When ``litellm.user_url_validation`` is False, validation is bypassed
    and this function delegates to ``client.get(url, follow_redirects=True)``.

    Args:
        client: An httpx.Client (sync).
        url: The user-supplied URL.

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Reproduce outside the proxy (curl -i) and inspect the 3xx response headers to confirm Location is absent/empty.
  2. Fix the origin server to send a valid Location header on redirect responses.
  3. If the status code was wrong (server meant 200), fix the server's status handling.
  4. As a workaround, request the final intended URL directly instead of following the broken redirect.

Example fix

# server before (Flask)
return "", 302  # no Location header

# after
from flask import redirect
return redirect("https://api.example.com/v1/file", code=302)
Defensive patterns

Strategy: try-catch

Validate before calling

def redirect_has_location(response) -> bool:
    if not response.is_redirect:
        return True
    loc = response.headers.get("location")
    return isinstance(loc, str) and bool(loc)

Try / catch

from litellm.litellm_core_utils.url_utils import SSRFError

try:
    resp = safe_get(client, url)
except SSRFError as e:
    if "no Location header" in str(e):
        return bad_gateway("upstream sent a malformed redirect")
    raise

Prevention

When it happens

Trigger: safe_get on a URL whose server returns a 301/302/307/308 with no Location header, an empty Location, or a non-string header value — e.g. hand-rolled test servers, misconfigured proxies stripping Location, or custom HTTP stacks returning header objects httpx treats as non-str.

Common situations: Test/mock servers returning status 302 without Location; reverse proxies (or security middleware) stripping redirect headers; buggy backend frameworks emitting redirect status codes by mistake; health checks that use 302 semantics without a target.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/88d476c4d8523514. Report an issue: GitHub.