BerriAI/litellm · error · Exception

Failed to fetch OIDC UserInfo: {e}

Error message

Failed to fetch OIDC UserInfo: {e}

What it means

Catch-all raised in get_userinfo: any exception during the UserInfo exchange that is not the explicit non-200 branch (network errors, DNS failures, TLS problems, timeouts, or unexpected parse/encoding errors) is logged ('Error fetching OIDC UserInfo: ...') and re-raised wrapped as 'Failed to fetch OIDC UserInfo: <cause>'. The chained cause text identifies the underlying failure.

Source

Thrown at litellm/proxy/auth/handle_jwt.py:783

            if response.status_code != 200:
                raise Exception(f"OIDC UserInfo endpoint returned status {response.status_code}: {response.text}")

            userinfo: Final = response.json()
            verbose_proxy_logger.debug("Received OIDC UserInfo: %s", userinfo)

            # Cache the userinfo response
            await self.user_api_key_cache.async_set_cache(
                key=cache_key,
                value=userinfo,
                ttl=self.litellm_jwtauth.oidc_userinfo_cache_ttl,
            )

            return userinfo

        except Exception as e:
            verbose_proxy_logger.error("Error fetching OIDC UserInfo: %s", e)
            raise Exception(f"Failed to fetch OIDC UserInfo: {e}")

    _unscoped_jwt_warning_emitted = False

    @classmethod
    def _build_decode_kwargs(cls) -> dict:
        """Build the audience/issuer/options kwargs for ``jwt.decode``.

        Setting ``JWT_AUDIENCE`` (and optionally ``JWT_ISSUER``) turns on the
        corresponding PyJWT verifications, blocking cross-tenant tokens
        minted by other applications that share the same IdP signing keys.
        When both are unset PyJWT only checks the signature and expiry, which
        is preserved for backward compatibility but logged once as a warning.

        The warning fires even in mixed deployments that also configure
        ``LiteLLM_JWTAuth.issuers``: tokens whose ``iss`` does not match any
        configured issuer fall through to this global path, and if env-var
        scoping is absent that fallback is itself unscoped.
        """

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Read the wrapped cause in the message - it distinguishes DNS vs connection vs SSL vs timeout
  2. From the proxy container, curl the UserInfo endpoint to verify network paths, DNS, and TLS trust
  3. Allowlist the IdP domain in egress rules / NetworkPolicies and add the IdP CA to the container trust store if using a private CA
  4. For slow IdPs, raise the HTTP client timeout used for UserInfo fetches
Defensive patterns

Strategy: retry

Validate before calling

import httpx, socket

def userinfo_endpoint_reachable(url: str) -> bool:
    host = httpx.URL(url).host
    try:
        socket.getaddrinfo(host, 443)
        return True
    except socket.gaierror:
        return False

Try / catch

import asyncio

async def fetch_userinfo_with_retry(fetch, attempts=3):
    for i in range(attempts):
        try:
            return await fetch()
        except Exception as e:
            if "Failed to fetch OIDC UserInfo" in str(e) and i < attempts - 1:
                await asyncio.sleep(2 ** i)  # transient network/DNS/TLS - back off
                continue
            raise

Prevention

When it happens

Trigger: The proxy cannot reach oidc_userinfo_endpoint at all - DNS resolution failure, connection refused, TLS handshake error, or HTTP client timeout - while handling a JWT-authenticated request that requires UserInfo claims.

Common situations: Egress firewall or NetworkPolicy blocking the IdP host from the proxy pod; wrong DNS/internal hostname for the IdP inside Kubernetes; self-signed cert on the IdP failing verification; IdP slow enough to hit the client timeout.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/4d20b4aaa863d794. Report an issue: GitHub.