{"record":{"id":"dda45b94a2a6e969","repo":"BerriAI/litellm","slug":"invalid-request","errorCode":"invalid_request","errorMessage":"invalid_request","messagePattern":"invalid_request","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/_experimental/mcp_server/oauth_utils.py","lineNumber":78,"sourceCode":"    error_description: str,\n    *,\n    hint: str | None = None,\n    **extra: Any,\n) -> NoReturn:\n    \"\"\"Raise ``invalid_request`` (RFC 6749) with a debuggable description.\n\n    FastAPI serializes ``detail`` as JSON. Callers still see ``error``:\n    ``invalid_request``; ``error_description`` and ``hint`` explain what\n    failed and how to fix it (e.g. reverse-proxy / PROXY_BASE_URL issues).\n    \"\"\"\n    detail: Final[dict[str, Any]] = {\n        \"error\": \"invalid_request\",\n        \"error_description\": error_description,\n    }\n    if hint:\n        detail[\"hint\"] = hint\n    detail.update(extra)\n    raise HTTPException(status_code=400, detail=detail)\n\n\ndef _origin_label(scheme: str, netloc: str) -> str:\n    \"\"\"Human-readable origin for error messages (scheme + host[:port]).\"\"\"\n    return f\"{scheme}://{netloc}\" if netloc else f\"{scheme}://\"\n\n\ndef _redact_mcp_resource_url(url: str | None) -> str | None:\n    \"\"\"Reduce an MCP server URL to its origin (scheme + host + port) for logging.\n\n    Everything else is dropped: userinfo (``user:pass@``), the query string, the\n    fragment, and the path, because hosted MCP servers routinely embed the\n    credential in the path (e.g. ``/mcp/s/<token>``) and this value is persisted\n    in spend-log metadata that a caller who can invoke the tool can read back.\n    Returns None when the URL has no host to identify (nothing safe to log).\n    \"\"\"\n    if not isinstance(url, str) or not url:\n        return None","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/_experimental/mcp_server/oauth_utils.py#L60-L96","documentation":"Raised by the shared MCP OAuth helper _oauth_invalid_request in the litellm proxy: any malformed OAuth request on the proxy-hosted MCP OAuth endpoints (authorize, token, DCR, BYOK callback) is converted into an RFC 6749 invalid_request HTTP 400. The JSON detail carries error, error_description, and often a hint naming the exact fix (frequently a reverse-proxy / PROXY_BASE_URL origin mismatch or a malformed redirect_uri). It is a request-shape/ops-configuration problem, not an upstream IdP failure.","triggerScenarios":"Any call to the MCP OAuth surface with a redirect_uri that: is not a valid absolute URL; uses a non-http(s) scheme not allowlisted in MCP_TRUSTED_NATIVE_REDIRECT_URIS (e.g. myapp://); contains a URL fragment, userinfo, a backslash in the host, or no host; is non-loopback where a loopback policy applies; or whose origin differs from the proxy's computed origin because PROXY_BASE_URL is wrong/unset or X-Forwarded-Proto/Host/Port are not forwarded through a reverse proxy.","commonSituations":"Running litellm behind nginx/traefik without proxy header forwarding, so the computed origin (e.g. http://internal:4000) never matches the https redirect_uri; PROXY_BASE_URL set to a different scheme/host than clients use; custom native MCP clients with cursor://-style callbacks not listed in MCP_TRUSTED_NATIVE_REDIRECT_URIS; first-party web apps on sister domains not listed in MCP_TRUSTED_REDIRECT_ORIGINS.","solutions":["Read the 400 body's error_description and hint first - they name the exact failed check (scheme, fragment, host, origin mismatch).","Behind a reverse proxy, set PROXY_BASE_URL to the public origin and/or forward X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port so the proxy's origin equals the redirect_uri origin.","For custom native client callbacks (e.g. myapp://host/oauth/callback), add the full URI to the MCP_TRUSTED_NATIVE_REDIRECT_URIS env var (use a trailing * for prefix match, ending the prefix with /).","For first-party apps on other domains, add host or host:port (optionally *.sub.domain) to the MCP_TRUSTED_REDIRECT_ORIGINS env var.","Ensure redirect_uri is an absolute http(s) URL with a host, no fragment, no user:pass@ userinfo, and no backslashes."],"exampleFix":"# before (proxy behind nginx on https://llm.example.com, PROXY_BASE_URL unset)\n# client redirect_uri=https://llm.example.com/ui/mcp/oauth/callback\n# -> 400 {\"error\":\"invalid_request\",\"error_description\":\"origin mismatch ...\"}\n\n# after\nexport PROXY_BASE_URL=\"https://llm.example.com\"\n# nginx:\n#   proxy_set_header Host $host;\n#   proxy_set_header X-Forwarded-Proto $scheme;","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef redirect_uri_ok(redirect_uri: str, proxy_origin: str) -> bool:\n    p = urlparse(redirect_uri)\n    if p.scheme not in (\"http\", \"https\"):\n        return False  # native schemes need MCP_TRUSTED_NATIVE_REDIRECT_URIS on the server\n    if p.fragment or not p.netloc or p.username or p.password or \"\\\\\" in p.netloc:\n        return False\n    host = (p.hostname or \"\").lower()\n    return f\"{p.scheme}://{host}\" == proxy_origin or host in (\"localhost\", \"127.0.0.1\")","typeGuard":null,"tryCatchPattern":"resp = await client.get(authorize_url, params=params)\nif resp.status_code == 400:\n    detail = resp.json()\n    if detail.get(\"error\") == \"invalid_request\":\n        raise McpOAuthConfigError(detail.get(\"hint\") or detail.get(\"error_description\")) from None\nraise_for_status(resp)","preventionTips":["Set PROXY_BASE_URL explicitly in every environment that sits behind a reverse proxy.","Forward X-Forwarded-Proto/Host/Port from your LB/ingress to the proxy.","Keep a checklist for redirect_uri shape: absolute http(s), host present, no fragment, no userinfo, no backslashes.","Register custom native callbacks in MCP_TRUSTED_NATIVE_REDIRECT_URIS and sister domains in MCP_TRUSTED_REDIRECT_ORIGINS at deploy time, not after the first 400."],"tags":["oauth","redirect-uri","reverse-proxy","proxy-base-url","http-400","mcp"],"backgroundTag":"oauth-invalid-redirect-uri","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}