BerriAI/litellm · critical · AzureOpenAIError

Rejected polling URL: {ssrf_err}

Error message

Rejected polling URL: {ssrf_err}

What it means

Security guard (VERIA-51) in Azure batch operation polling: after a submission response, LiteLLM reads the `operation-location` header and requires its origin to match the configured `api_base`. If origins differ, it raises this 502 instead of following the URL, because polling an attacker-controlled URL would send the operator's Azure API key in request headers (blind SSRF → credential leak).

Source

Thrown at litellm/llms/azure/azure.py:903

                "model", None
            )  # REMOVE 'model' from dall-e-2 arg https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#request-a-generated-image-dall-e-2-preview
            response = await async_handler.post(
                url=api_base,
                data=json.dumps(data),
                headers=headers,
            )
            if "operation-location" in response.headers:
                operation_location_url: Final = response.headers["operation-location"]
            else:
                raise AzureOpenAIError(status_code=500, message=response.text)
            # Reject polling URLs that don't share an origin with ``api_base``.
            # Without this an upstream-controlled or attacker-controlled
            # value would receive the operator's Azure API key in the
            # request headers below. VERIA-51.
            try:
                assert_same_origin(operation_location_url, api_base)
            except SSRFError as ssrf_err:
                raise AzureOpenAIError(
                    status_code=502,
                    message=f"Rejected polling URL: {ssrf_err}",
                )
            response = await async_handler.get(
                url=operation_location_url,
                headers=headers,
            )

            await response.aread()

            timeout_secs: Final[int] = AZURE_OPERATION_POLLING_TIMEOUT
            start_time: Final = time.time()
            if "status" not in response.json():
                # Don't reflect the raw response body — when the polling
                # URL points at an internal JSON API (cloud metadata
                # service etc.) reflecting it here turns Blind SSRF into
                # Full-Read SSRF. VERIA-51.
                raise AzureOpenAIError(

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Set api_base to the exact Azure OpenAI endpoint origin that Azure will echo back in operation-location (scheme+host+port must match).
  2. If a gateway must be used, make it preserve/rewrite operation-location to its own origin consistently.
  3. Verify with curl -i that the operation-location header origin equals your configured api_base origin.
  4. Never 'fix' this by disabling the check — it protects your API key from exfiltration.

Example fix

# before
litellm.completion(model='azure/gpt-4o', messages=msgs, api_base='https://gw.internal.example.com')  # gateway origin != operation-location origin

# after
litellm.completion(model='azure/gpt-4o', messages=msgs, api_base='https://myres.openai.azure.com')  # same origin as operation-location
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

def same_origin(a: str, b: str) -> bool:
    pa, pb = urlparse(a), urlparse(b)
    return (pa.scheme, pa.hostname, pa.port) == (pb.scheme, pb.hostname, pb.port)

# before batch calls, assert your api_base matches where operation-location will point:
assert same_origin('https://myres.openai.azure.com', api_base), 'api_base origin must match Azure operation-location origin'

Try / catch

from litellm.exceptions import APIError

try:
    result = litellm.poll_batch(job_id)
except APIError as e:
    if 'Rejected polling URL' in str(e):
        # config problem: fix api_base origin; do NOT retry blindly
        raise RuntimeError('api_base origin differs from operation-location; align api_base with the Azure endpoint')
    raise

Prevention

When it happens

Trigger: Azure (or something on the path) returns an operation-location header pointing to a different origin than api_base — e.g. api_base set to a gateway/Caddy domain while Azure returns its native endpoint, or a response-relaying proxy that rewrites Location headers.

Common situations: Using a custom domain or API gateway in front of Azure OpenAI; Azure returning regional failover endpoints that differ from the configured base; man-in-the-middle or compromised upstream rewriting headers (the attack this guard blocks).

Related errors


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