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
- Set api_base to the exact Azure OpenAI endpoint origin that Azure will echo back in operation-location (scheme+host+port must match).
- If a gateway must be used, make it preserve/rewrite operation-location to its own origin consistently.
- Verify with curl -i that the operation-location header origin equals your configured api_base origin.
- 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
- Use the native Azure OpenAI endpoint as api_base for batch operations; avoid origin-rewriting gateways on this path.
- Never disable or monkeypatch assert_same_origin to 'get it working' — it prevents API key exfiltration.
- If a gateway is mandatory, make it consistently rewrite operation-location to its own origin.
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
- Polling response missing 'status' field
- Azure Document Intelligence: rejected polling URL ({ssrf_err
- MAVVRIK_API_ENDPOINT host must be a Mavvrik domain (e.g. htt
- Mavvrik FOCUS destination: {label} must be a GCS endpoint (s
- {field_name} cannot be a dot path segment
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/f46ac24414066488.
Report an issue: GitHub.