Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_experimental/mcp_server/faults/render_oauth.py: 16%
50 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""Render upstream OAuth/DCR faults onto the wire. The only place that chooses statuses and bodies
2for these faults, so every consumer emits the same contract: RFC 6749 §5.2-shaped JSON with the §5.1
3no-store headers on token endpoints, HTTPException details on registration. Status, code, and prose
4all derive from the fault tag; exhaustive matches keep a new fault arm from shipping unrendered.
5"""
7from __future__ import annotations
9from typing import Final
11from fastapi.responses import JSONResponse
12from typing_extensions import assert_never
14from litellm.proxy._experimental.mcp_server.faults.types import CallerRejected, UpstreamOAuthFault
15from litellm.proxy._experimental.mcp_server.oauth_utils import TOKEN_NO_CACHE_HEADERS
18def _gateway_rejected_description(code: str) -> str:
19 if code == "invalid_target":
20 return (
21 "the upstream authorization server rejected the request (invalid_target); it did not "
22 "accept this server's RFC 8707 resource indicator. Set upstream_resource on the MCP "
23 "server to the resource identifier the authorization server expects, or unset it if "
24 "that authorization server does not support resource indicators"
25 )
26 return (
27 f"the upstream authorization server rejected the gateway's configured client credentials "
28 f"({code}); verify the MCP server's client_id and client_secret"
29 )
32def _upstream_reported_status_and_description(code: str) -> tuple[int, str]:
33 if code == "temporarily_unavailable":
34 return 503, "the upstream authorization server is temporarily unavailable; retry shortly"
35 return 502, "the upstream authorization server reported an internal error"
38def _registration_refused_description(status_code: int) -> str:
39 return (
40 f"the upstream authorization server refused dynamic client registration (HTTP {status_code}). "
41 "This provider may require a pre-registered OAuth client. Configure client_id and, if required "
42 "by the provider, client_secret for this MCP server to skip dynamic registration"
43 )
46def _render_caller_rejected(fault: CallerRejected) -> JSONResponse:
47 content: Final = {
48 "error": fault.code,
49 **({"error_description": fault.description} if fault.description else {}),
50 **({"error_uri": fault.error_uri} if fault.error_uri else {}),
51 }
52 status_code: Final = 401 if fault.code == "invalid_client" else 400
53 return JSONResponse(status_code=status_code, content=content, headers=TOKEN_NO_CACHE_HEADERS)
56def render_token_fault(fault: UpstreamOAuthFault) -> JSONResponse:
57 """RFC 6749 §5.2 response for a token-endpoint fault. Caller-actionable rejections relay the
58 upstream's code on the status that code implies (401 for invalid_client per §5.2, else 400);
59 gateway-side faults are 502 ``server_error`` with gateway-authored prose so a caller is never
60 blamed for, or shown the internals of, a failure only the operator can fix."""
61 match fault.tag:
62 case "caller_rejected":
63 return _render_caller_rejected(fault)
64 case "gateway_rejected":
65 return JSONResponse(
66 status_code=502,
67 content={
68 "error": "server_error",
69 "error_description": _gateway_rejected_description(fault.code),
70 },
71 headers=TOKEN_NO_CACHE_HEADERS,
72 )
73 case "upstream_reported_fault":
74 status_code, description = _upstream_reported_status_and_description(fault.code)
75 return JSONResponse(
76 status_code=status_code,
77 content={"error": fault.code, "error_description": description},
78 headers=TOKEN_NO_CACHE_HEADERS,
79 )
80 case "upstream_registration_refused":
81 return _render_caller_rejected(
82 CallerRejected(
83 code="unauthorized_client",
84 description=_registration_refused_description(fault.status_code),
85 )
86 )
87 case "upstream_protocol_fault":
88 return JSONResponse(
89 status_code=502,
90 content={"error": "server_error", "error_description": fault.note},
91 headers=TOKEN_NO_CACHE_HEADERS,
92 )
93 case _:
94 assert_never(fault.tag)
97def dcr_fault_detail(fault: UpstreamOAuthFault) -> tuple[int, str]:
98 """Status and detail string for a registration fault, raised as HTTPException by the caller.
99 RFC 7591 §3.2.2 defines registration errors as 400, so a contract-conformant rejection is 400
100 regardless of the upstream status; a bare 401/403 is a registration refusal rendered as 403."""
101 match fault.tag:
102 case "caller_rejected":
103 detail: Final = f"{fault.code}: {fault.description}" if fault.description else fault.code
104 return 400, detail
105 case "gateway_rejected":
106 return 502, _gateway_rejected_description(fault.code)
107 case "upstream_reported_fault":
108 return _upstream_reported_status_and_description(fault.code)
109 case "upstream_registration_refused":
110 return 403, _registration_refused_description(fault.status_code)
111 case "upstream_protocol_fault":
112 return 502, fault.note
113 case _:
114 assert_never(fault.tag)