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(): """