From 0199e404f2747155765ffbba91e0f0ae50c319c2 Mon Sep 17 00:00:00 2001 From: Kiyeon Jeon Date: Fri, 10 Apr 2026 23:35:28 +0900 Subject: [PATCH] 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 --- litellm/proxy/auth/auth_exception_handler.py | 16 ++++-- .../proxy/auth/test_auth_exception_handler.py | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py index 9c306acd2c6..b34cd1d12b4 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -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, diff --git a/tests/test_litellm/proxy/auth/test_auth_exception_handler.py b/tests/test_litellm/proxy/auth/test_auth_exception_handler.py index 3e780c6ee92..78956837ef3 100644 --- a/tests/test_litellm/proxy/auth/test_auth_exception_handler.py +++ b/tests/test_litellm/proxy/auth/test_auth_exception_handler.py @@ -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(): """