BerriAI/litellm · error · HTTPException

Microsoft Purview DLP: upstream policy evaluation failed

Error message

Microsoft Purview DLP: upstream policy evaluation failed

What it means

Runtime HTTPException raised from the Purview guardrail hook when the upstream Microsoft Purview DLP policy-evaluation call returns an HTTP error status (httpx.HTTPStatusError). LiteLLM maps upstream 401/403 to client-facing 502 (a proxy-side credential/consent problem the caller cannot fix), passes other upstream statuses through, and forwards any Retry-After header. It only raises when block_on_violation/fail-closed mode is on; in logging-only mode the same failure is just logged and the request proceeds.

Source

Thrown at litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py:155

        except HTTPException:
            status = "guardrail_failed_to_respond"
            raise
        except httpx.HTTPStatusError as exc:
            # Preserve the upstream Graph API status code (e.g. 429, 503) so
            # callers can distinguish a transient infrastructure error from a
            # DLP policy block (signaled separately as HTTP 400 below) and can
            # implement retry-after handling on rate limits.  401/403 upstream
            # responses indicate a proxy-side credential / consent problem the
            # caller can do nothing about, so they are mapped to 502.
            status = "guardrail_failed_to_respond"
            if block_on_violation:
                upstream_status: Final = exc.response.status_code
                client_status: Final = 502 if upstream_status in (401, 403) else upstream_status
                headers: dict[str, str] | None = None
                retry_after: Final = exc.response.headers.get("retry-after")
                if retry_after:
                    headers = {"Retry-After": retry_after}
                raise HTTPException(
                    status_code=client_status,
                    detail={
                        "error": "Microsoft Purview DLP: upstream policy evaluation failed",
                        "activity": activity,
                        "upstream_status": upstream_status,
                        "exception": str(exc),
                    },
                    headers=headers,
                ) from exc
            verbose_proxy_logger.warning(
                "Purview DLP: API/network error in logging-only mode (not re-raised): %s",
                exc,
            )
        except Exception as exc:
            status = "guardrail_failed_to_respond"
            if block_on_violation:
                raise HTTPException(
                    status_code=400,

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. For 502 responses: re-check tenant_id/client_id/client_secret in the guardrail config, rotate the secret if expired, and verify the app registration's Purview API permissions + admin consent
  2. For 429: honor the forwarded Retry-After header, add jittered client-side retry, and reduce request rate or scale Purview quota
  3. For passthrough 5xx: check Azure status / retry idempotent requests
  4. Temporarily set block_on_violation/log-only mode to keep traffic flowing while diagnosing, then re-enable fail-closed

Example fix

# before (client, no retry-aware handling)
resp = client.chat.completions.create(model="gpt-4o", messages=msgs)

# after: retry on 429/5xx honoring Retry-After
import time, httpx
for attempt in range(5):
    try:
        resp = client.chat.completions.create(model="gpt-4o", messages=msgs)
        break
    except httpx.HTTPStatusError as e:
        sc = e.response.status_code
        if sc in (429, 500, 502, 503) and attempt < 4:
            time.sleep(float(e.response.headers.get("retry-after", 2 ** attempt)))
            continue
        raise
Defensive patterns

Strategy: retry

Validate before calling

# Before relying on Purview at runtime, smoke-test the credential path once at startup
import httpx

def purview_credentials_reachable(cfg) -> bool:
    # token fetch exercises tenant/client/secret without touching DLP policy
    r = httpx.post(
        f"https://login.microsoftonline.com/{cfg['tenant_id']}/oauth2/v2.0/token",
        data={"client_id": cfg["client_id"], "client_secret": cfg["client_secret"],
              "grant_type": "client_credentials", "scope": "https://purview.azure.net/.default"},
    )
    return r.status_code == 200

Try / catch

# Client-side: classify by mapped status
# 502  -> upstream 401/403, proxy credential problem: do NOT retry, alert operator
# 429  -> retry honoring Retry-After (the proxy forwards it)
# 5xx  -> retry with backoff
for attempt in range(5):
    try:
        out = call_llm(payload)
        break
    except httpx.HTTPStatusError as e:
        sc = e.response.status_code
        if sc == 502 and "Purview" in e.response.text:
            alert("purview credentials broken"); raise
        if sc in (429, 500, 502, 503) and attempt < 4:
            time.sleep(float(e.response.headers.get("retry-after", 2 ** attempt))); continue
        raise

Prevention

When it happens

Trigger: Expired Entra client secret or wrong tenant/client_id → Purview returns 401 → client sees 502; missing Purview consent/DLP permissions → 403 → 502; Purview throttling → 429 passes through with Retry-After; Purview 5xx or 400 passes through verbatim

Common situations: Secret rotated in Azure but not in config.yaml; Purview DLP policies not onboarded or the app lacks the required API permissions; network egress blocked so the token or evaluation call fails; intermittent 429s under load

Related errors


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