Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/auth/auth_exception_handler.py: 72%
74 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"""
2Handles Authentication Errors
3"""
5import logging
6from collections.abc import Mapping
7from typing import TYPE_CHECKING, Any, Final
9from fastapi import HTTPException, Request, status
11import litellm
12from litellm._logging import verbose_proxy_logger, verbose_proxy_stdout_logger
13from litellm.constants import EMPTY_MAPPING
14from litellm.integrations.otel.runtime import seed_request_identity
15from litellm.litellm_core_utils.core_helpers import is_expected_client_error
16from litellm.proxy._types import (
17 LitellmUserRoles,
18 ModelAccessDeniedProxyException,
19 ProxyErrorTypes,
20 ProxyException,
21 UserAPIKeyAuth,
22)
23from litellm.proxy.auth.auth_utils import (
24 _get_request_ip_address,
25 is_invalid_virtual_key_error,
26 mark_invalid_virtual_key_error,
27 normalize_request_route,
28)
29from litellm.proxy.auth.model_access_denied import ModelAccessDeniedHTTPException
30from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
31from litellm.types.services import ServiceTypes
33# Sentinel user_id for the synthetic UserAPIKeyAuth issued during a DB
34# outage when allow_requests_on_db_unavailable is True. Downstream
35# enforcement can key off this value; it must never collide with a real
36# user_id.
37DB_UNAVAILABLE_FALLBACK_USER_ID: Final = "__db_unavailable_fallback__"
39if TYPE_CHECKING: 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true
40 from opentelemetry.trace import Span as _Span
42 Span = _Span | Any
43else:
44 Span = Any
47def _as_proxy_exception(e: Exception) -> ProxyException:
48 """Convert an authentication failure into the ProxyException the client receives."""
49 if isinstance(e, litellm.BudgetExceededError): 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true
50 return ProxyException(
51 message=e.message,
52 type=ProxyErrorTypes.budget_exceeded,
53 param=None,
54 code=getattr(e, "status_code", status.HTTP_429_TOO_MANY_REQUESTS),
55 )
56 if isinstance(e, ModelAccessDeniedHTTPException): 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 return ModelAccessDeniedProxyException(
58 message=str(e.detail),
59 internal_message=e.internal_message,
60 type=ProxyErrorTypes.auth_error,
61 param="None",
62 code=e.status_code,
63 )
64 if isinstance(e, HTTPException):
65 return ProxyException(
66 message=getattr(e, "detail", f"Authentication Error({e})"),
67 type=ProxyErrorTypes.auth_error,
68 param=getattr(e, "param", "None"),
69 code=getattr(e, "status_code", status.HTTP_401_UNAUTHORIZED),
70 )
71 if isinstance(e, ProxyException): 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 return e
73 if PrismaDBExceptionHandler.is_database_service_unavailable_error(e): 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true
74 return PrismaDBExceptionHandler.service_unavailable_proxy_exception(e)
75 return ProxyException(
76 message="Authentication Error, " + str(e),
77 type=ProxyErrorTypes.auth_error,
78 param=getattr(e, "param", "None"),
79 code=status.HTTP_401_UNAUTHORIZED,
80 )
83def _get_user_agent(request: Request) -> str | None:
84 if "headers" not in request.scope: 84 ↛ 85line 84 didn't jump to line 85 because the condition on line 84 was never true
85 return None
86 return request.headers.get("user-agent")
89def _with_client_context(
90 request_data: dict[str, object], requester_ip: str | None, user_agent: str | None
91) -> dict[str, object]:
92 """Auth gate rejections are raised before `add_litellm_data_to_request` records the
93 caller IP and User-Agent, so their failure logs would otherwise carry neither."""
94 key: Final = "litellm_metadata" if "litellm_metadata" in request_data else "metadata"
95 metadata: Final = request_data.get(key)
96 base: Final[Mapping[str, object]] = metadata if isinstance(metadata, Mapping) else EMPTY_MAPPING
97 stamped: Final = {
98 name: value
99 for name, value in (("requester_ip_address", requester_ip), ("user_agent", user_agent))
100 if value and not base.get(name)
101 }
102 if not stamped: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 return request_data
104 return {**request_data, key: {**base, **stamped}} # mutable-ok: logging needs dicts
107class UserAPIKeyAuthExceptionHandler:
108 @staticmethod
109 async def _handle_authentication_error(
110 e: Exception,
111 request: Request,
112 request_data: dict[str, object],
113 route: str,
114 parent_otel_span: Span | None,
115 api_key: str,
116 resolved_identity: UserAPIKeyAuth | None = None,
117 ) -> UserAPIKeyAuth:
118 """
119 Handles Connection Errors when reading a Virtual Key from LiteLLM DB
120 Use this if you don't want failed DB queries to block LLM API reqiests
122 Reliability scenarios this covers:
123 - DB is down and having an outage
124 - Unable to read / recover a key from the DB
126 Returns:
127 - UserAPIKeyAuth: If general_settings.allow_requests_on_db_unavailable is True
129 Raises:
130 - Original Exception in all other cases
131 """
132 from litellm.proxy.proxy_server import (
133 general_settings,
134 proxy_logging_obj,
135 )
137 if ( 137 ↛ 142line 137 didn't jump to line 142 because the condition on line 137 was never true
138 PrismaDBExceptionHandler.should_allow_request_on_db_unavailable()
139 and PrismaDBExceptionHandler.is_database_connection_error(e)
140 ):
141 # log this as a DB failure on prometheus
142 proxy_logging_obj.service_logging_obj.service_failure_hook(
143 service=ServiceTypes.DB,
144 call_type="get_key_object",
145 error=e,
146 duration=0.0,
147 )
149 # Non-admin restricted token so a DB outage cannot escalate
150 # an anonymous caller to proxy-admin privileges.
151 verbose_proxy_logger.warning(
152 "Auth: DB unavailable — issuing restricted INTERNAL_USER "
153 "fallback token (allow_requests_on_db_unavailable=True)"
154 )
155 return UserAPIKeyAuth(
156 key_name="failed-to-connect-to-db",
157 token="failed-to-connect-to-db",
158 user_id=DB_UNAVAILABLE_FALLBACK_USER_ID,
159 user_role=LitellmUserRoles.INTERNAL_USER,
160 request_route=route,
161 )
162 else:
163 # raise the exception to the caller
164 requester_ip: Final = _get_request_ip_address(
165 request=request,
166 use_x_forwarded_for=general_settings.get("use_x_forwarded_for") is True,
167 )
168 user_agent: Final = _get_user_agent(request)
170 # Log authentication failures before identity seeding and callbacks, so the log
171 # survives a raising callback pipeline. Classify and route malformed virtual-key
172 # rejections to WARNING on stdout (suppressible via LITELLM_LOG=ERROR).
173 log_extra: Final = {"requester_ip": requester_ip}
174 is_invalid_virtual_key: Final = is_invalid_virtual_key_error(e)
175 is_quiet_log: Final = is_invalid_virtual_key and not litellm.log_client_error_tracebacks
176 logger: Final = verbose_proxy_stdout_logger if is_quiet_log else verbose_proxy_logger
177 logger.log(
178 logging.WARNING if is_quiet_log else logging.ERROR,
179 "litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - %s\nRequester IP Address:%s",
180 e,
181 requester_ip,
182 exc_info=True if litellm.log_client_error_tracebacks or not is_expected_client_error(e) else None,
183 extra=log_extra,
184 )
186 # Log this exception to OTEL, Datadog etc. Reuse the identity resolved
187 # before the failure (team alias/id, metadata, user) so the failed span
188 # is labeled — a fresh UserAPIKeyAuth here would drop everything auth had
189 # already looked up (e.g. an expired key whose team/user is known). Copy
190 # so the handler is side-effect-free for the caller's identity object.
191 user_api_key_dict = resolved_identity.model_copy() if resolved_identity is not None else UserAPIKeyAuth()
192 user_api_key_dict.parent_otel_span = parent_otel_span
193 user_api_key_dict.request_route = normalize_request_route(route)
194 user_api_key_dict.api_key = user_api_key_dict.api_key or UserAPIKeyAuth(api_key=api_key).api_key
196 # Stamp identity onto the request's server span now, before the request
197 # is rejected; the OTEL failure hooks don't touch the server span, so
198 # without this the failed trace would carry no team/key attributes.
199 seed_request_identity(
200 user_api_key_dict,
201 model=request_data.get("model"),
202 )
204 # Budget checks live in tenant-scoped helpers (key / team / org / tag)
205 # that don't see the request model, so the BudgetExceededError they
206 # raise carries `llm_provider=""`. Resolve it here off `request_data`
207 # so custom-callback consumers reading StandardLoggingPayload get
208 # the same `llm_provider` attribution as for RPM/TPM 429s.
209 if isinstance(e, litellm.BudgetExceededError) and not e.llm_provider: 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true
210 from litellm.proxy.hooks.rate_limiter_utils import (
211 resolve_llm_provider_for_rate_limit,
212 )
214 budget_model: Final = request_data.get("model")
215 _, e.llm_provider = resolve_llm_provider_for_rate_limit(
216 budget_model if isinstance(budget_model, str) else None
217 )
219 # Allow callbacks to transform the error response
220 transformed_exception: Final = await proxy_logging_obj.post_call_failure_hook(
221 request_data=_with_client_context(request_data, requester_ip, user_agent),
222 original_exception=e,
223 user_api_key_dict=user_api_key_dict,
224 error_type=ProxyErrorTypes.auth_error,
225 route=route,
226 )
227 # Use transformed exception if callback returned one, otherwise use original
228 if transformed_exception is not None: 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true
229 e = transformed_exception
231 final_exception: Final = mark_invalid_virtual_key_error(_as_proxy_exception(e), is_invalid_virtual_key)
232 # If a quiet-logged malformed-key transform yields non-401, escalate to ERROR
233 if is_quiet_log and str(final_exception.code) != str(status.HTTP_401_UNAUTHORIZED): 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true
234 verbose_proxy_logger.error(
235 "litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - %s\nRequester IP Address:%s",
236 final_exception,
237 requester_ip,
238 extra=log_extra,
239 )
240 raise final_exception