fix(proxy): log exception message and request context in auth_exception_handler

ProxyException does not define __str__, so str(e) produces an empty
string in the error log at auth_exception_handler.py:80. Switch to
getattr(e, "message", str(e)) which resolves the actual error text
for ProxyException while remaining safe for standard exceptions.

Additionally surface route and model in the log line and structured
extra dict so operators can correlate auth failures without digging
through raw pod logs.

Apply the same getattr pattern to the two remaining str(e) sites in
the HTTPException and generic-exception branches for consistency.

Fixes #25361
This commit is contained in:
Kiyeon Jeon 2026-04-10 23:35:28 +09:00
parent 9e6d2d2069
commit 0199e404f2
2 changed files with 66 additions and 5 deletions

View file

@ -76,11 +76,17 @@ class UserAPIKeyAuthExceptionHandler:
use_x_forwarded_for=general_settings.get("use_x_forwarded_for", False),
)
verbose_proxy_logger.exception(
"litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - {}\nRequester IP Address:{}".format(
str(e),
"litellm.proxy.proxy_server.user_api_key_auth(): Exception occured - {}\nRequester IP Address:{}\nRoute: {}\nModel: {}".format(
getattr(e, "message", str(e)),
requester_ip,
route,
request_data.get("model", "unknown"),
),
extra={"requester_ip": requester_ip},
extra={
"requester_ip": requester_ip,
"route": route,
"model": request_data.get("model"),
},
)
# Log this exception to OTEL, Datadog etc
@ -110,7 +116,7 @@ class UserAPIKeyAuthExceptionHandler:
)
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "detail", f"Authentication Error({str(e)})"),
message=getattr(e, "detail", f"Authentication Error({getattr(e, 'message', str(e))})"),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_401_UNAUTHORIZED),
@ -118,7 +124,7 @@ class UserAPIKeyAuthExceptionHandler:
elif isinstance(e, ProxyException):
raise e
raise ProxyException(
message="Authentication Error, " + str(e),
message="Authentication Error, " + getattr(e, "message", str(e)),
type=ProxyErrorTypes.auth_error,
param=getattr(e, "param", "None"),
code=status.HTTP_401_UNAUTHORIZED,

View file

@ -112,6 +112,61 @@ async def test_handle_authentication_error_budget_exceeded():
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
@pytest.mark.asyncio
async def test_proxy_exception_message_logged_correctly():
"""
Regression test for #25361 — ProxyException.message must appear in the log,
not an empty string from str(ProxyException(...)).
"""
handler = UserAPIKeyAuthExceptionHandler()
proxy_exc = ProxyException(
message="Model not allowed for this key",
type=ProxyErrorTypes.auth_error,
param=None,
code=401,
)
# Confirm the root cause: str() on ProxyException is empty
assert str(proxy_exc) == ""
# And that .message holds the real text
assert proxy_exc.message == "Model not allowed for this key"
mock_request = MagicMock()
mock_request_data = {"model": "gpt-4o"}
mock_route = "/v1/chat/completions"
mock_span = None
mock_api_key = "sk-test"
with patch(
"litellm.proxy.proxy_server.general_settings",
{"allow_requests_on_db_unavailable": False},
), patch(
"litellm.proxy.proxy_server.proxy_logging_obj.post_call_failure_hook",
new_callable=AsyncMock,
return_value=None,
), patch.object(
verbose_proxy_logger, "exception"
) as mock_log:
with pytest.raises(ProxyException):
await handler._handle_authentication_error(
proxy_exc,
mock_request,
mock_request_data,
mock_route,
mock_span,
mock_api_key,
)
assert mock_log.called
log_message = mock_log.call_args[0][0]
assert "Model not allowed for this key" in log_message, (
f"Expected exception message in log, got: {log_message!r}"
)
assert "/v1/chat/completions" in log_message
assert "gpt-4o" in log_message
@pytest.mark.asyncio
async def test_route_passed_to_post_call_failure_hook():
"""