{"record":{"id":"88d476c4d8523514","repo":"BerriAI/litellm","slug":"redirect-response-has-no-location-header","errorCode":null,"errorMessage":"Redirect response has no Location header","messagePattern":"Redirect response has no Location header","errorType":"exception","errorClass":"SSRFError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/url_utils.py","lineNumber":370,"sourceCode":"    expected_host: Final = _normalize_host(expected.hostname or \"\")\n    if not candidate_host or candidate_host != expected_host:\n        raise SSRFError(\"Origin mismatch on host\")\n\n    default_port: Final = 443 if candidate.scheme == \"https\" else 80\n    candidate_port: Final = candidate.port if candidate.port is not None else default_port\n    expected_port: Final = expected.port if expected.port is not None else default_port\n    if candidate_port != expected_port:\n        raise SSRFError(\"Origin mismatch on port\")\n\n\n_MAX_REDIRECTS: Final = 10\n\n\ndef _extract_redirect_url(response: Any, request_url: str) -> str:\n    \"\"\"Extract and resolve the redirect target from a response's Location header.\"\"\"\n    location: Final = response.headers.get(\"location\")\n    if not isinstance(location, str) or not location:\n        raise SSRFError(\"Redirect response has no Location header\")\n    # Resolve relative URLs against the request URL\n    return str(httpx.URL(request_url).join(location))\n\n\ndef safe_get(client: Any, url: str, **kwargs: Any) -> Any:\n    \"\"\"\n    Fetch a user-supplied URL with SSRF protection on every redirect hop.\n\n    Validates the initial URL and each redirect target before making the\n    request. No DNS rebinding (resolve-and-rewrite). No redirect bypass\n    (each hop validated). No breaking change for legitimate CDN redirects.\n\n    When ``litellm.user_url_validation`` is False, validation is bypassed\n    and this function delegates to ``client.get(url, follow_redirects=True)``.\n\n    Args:\n        client: An httpx.Client (sync).\n        url: The user-supplied URL.","sourceCodeStart":352,"sourceCodeEnd":388,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/litellm_core_utils/url_utils.py#L352-L388","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Reproduce outside the proxy (curl -i) and inspect the 3xx response headers to confirm Location is absent/empty.","Fix the origin server to send a valid Location header on redirect responses.","If the status code was wrong (server meant 200), fix the server's status handling.","As a workaround, request the final intended URL directly instead of following the broken redirect."],"exampleFix":"# server before (Flask)\nreturn \"\", 302  # no Location header\n\n# after\nfrom flask import redirect\nreturn redirect(\"https://api.example.com/v1/file\", code=302)","handlingStrategy":"try-catch","validationCode":"def redirect_has_location(response) -> bool:\n    if not response.is_redirect:\n        return True\n    loc = response.headers.get(\"location\")\n    return isinstance(loc, str) and bool(loc)","typeGuard":null,"tryCatchPattern":"from litellm.litellm_core_utils.url_utils import SSRFError\n\ntry:\n    resp = safe_get(client, url)\nexcept SSRFError as e:\n    if \"no Location header\" in str(e):\n        return bad_gateway(\"upstream sent a malformed redirect\")\n    raise","preventionTips":["Always set Location on 3xx responses in your servers.","Check proxies/middleware are not stripping Location.","Return 502-style errors for malformed upstream redirects instead of retrying."],"tags":["ssrf","redirect","http","location-header","malformed-response"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}