From 7445071dd2039a5566efa90b186299a7b18bafc9 Mon Sep 17 00:00:00 2001 From: Vansh Date: Sat, 3 Oct 2026 23:10:40 +0530 Subject: [PATCH 1/3] fix(proxy): safely extract status codes to prevent swallowed vllm exceptions --- .../exception_mapping_utils.py | 39 +++++++++++------ .../test_exception_mapping_utils.py | 42 +++++++++++++++++++ 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 0fdfb301291..04640549561 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -419,8 +419,21 @@ def _map_openai_exception( request=_request, litellm_debug_info=extra_information, ) - elif hasattr(original_exception, "status_code"): - if original_exception.status_code == 400: + _status_code = None + try: + if hasattr(original_exception, "status_code") and getattr(original_exception, "status_code", None) is not None: + _status_code = int(original_exception.status_code) + elif ( + hasattr(original_exception, "response") + and hasattr(original_exception.response, "status_code") + and getattr(original_exception.response, "status_code", None) is not None + ): + _status_code = int(original_exception.response.status_code) + except (ValueError, TypeError, AttributeError): + pass + + if _status_code is not None: + if _status_code == 400: raise BadRequestError( message=f"{exception_provider} - {message}", llm_provider=custom_llm_provider, @@ -429,7 +442,7 @@ def _map_openai_exception( litellm_debug_info=extra_information, body=getattr(original_exception, "body", None), ) - elif original_exception.status_code == 401: + elif _status_code == 401: raise AuthenticationError( message=f"AuthenticationError: {exception_provider} - {message}", llm_provider=custom_llm_provider, @@ -437,7 +450,7 @@ def _map_openai_exception( response=response, litellm_debug_info=extra_information, ) - elif original_exception.status_code == 404: + elif _status_code == 404: raise NotFoundError( message=f"NotFoundError: {exception_provider} - {message}", model=model, @@ -445,14 +458,14 @@ def _map_openai_exception( response=response, litellm_debug_info=extra_information, ) - elif original_exception.status_code == 408: + elif _status_code == 408: raise Timeout( message=f"Timeout Error: {exception_provider} - {message}", model=model, llm_provider=custom_llm_provider, litellm_debug_info=extra_information, ) - elif original_exception.status_code == 422: + elif _status_code == 422: raise BadRequestError( message=f"{exception_provider} - {message}", model=model, @@ -461,7 +474,7 @@ def _map_openai_exception( litellm_debug_info=extra_information, body=getattr(original_exception, "body", None), ) - elif original_exception.status_code == 429: + elif _status_code == 429: raise RateLimitError( message=f"RateLimitError: {exception_provider} - {message}", model=model, @@ -470,7 +483,7 @@ def _map_openai_exception( litellm_debug_info=extra_information, body=getattr(original_exception, "body", None), ) - elif original_exception.status_code == 500: + elif _status_code == 500: raise InternalServerError( message=f"InternalServerError: {exception_provider} - {message}", model=model, @@ -479,7 +492,7 @@ def _map_openai_exception( litellm_debug_info=extra_information, body=getattr(original_exception, "body", None), ) - elif original_exception.status_code == 502: + elif _status_code == 502: raise BadGatewayError( message=f"BadGatewayError: {exception_provider} - {message}", model=model, @@ -487,7 +500,7 @@ def _map_openai_exception( response=response, litellm_debug_info=extra_information, ) - elif original_exception.status_code == 503: + elif _status_code == 503: raise ServiceUnavailableError( message=f"ServiceUnavailableError: {exception_provider} - {message}", model=model, @@ -495,17 +508,17 @@ def _map_openai_exception( response=response, litellm_debug_info=extra_information, ) - elif original_exception.status_code == 504: # gateway timeout error + elif _status_code == 504: # gateway timeout error raise Timeout( message=f"Timeout Error: {exception_provider} - {message}", model=model, llm_provider=custom_llm_provider, litellm_debug_info=extra_information, - exception_status_code=original_exception.status_code, + exception_status_code=_status_code, ) else: raise APIError( - status_code=original_exception.status_code, + status_code=_status_code, message=f"APIError: {exception_provider} - {message}", llm_provider=custom_llm_provider, model=model, diff --git a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py index 9de768ea47b..0370ee967cb 100644 --- a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py +++ b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py @@ -1573,3 +1573,45 @@ def test_guardrail_provider_failure_status_is_still_mapped(): ) assert exc_info.value is not upstream_failure + + +def test_exception_mapping_vllm_nested_status_code(): + """ + Test that when an exception wraps a status_code inside a response object + (or as a string), we safely extract it and return the correct mapped error + (e.g., 500 InternalServerError) rather than swallowing it or returning a + generic APIConnectionError. + """ + + class MockResponse: + def __init__(self, code): + self.status_code = code + + class RawHTTPError(Exception): + def __init__(self, response): + self.response = response + super().__init__("Mock HTTP Error") + + # 1. Test with integer status code tucked in response + crash_error = RawHTTPError(MockResponse(500)) + with pytest.raises(litellm.InternalServerError) as exc_info: + exception_type( + model="test-model", + original_exception=crash_error, + custom_llm_provider="hosted_vllm", + completion_kwargs={}, + extra_kwargs={}, + ) + assert exc_info.value.status_code == 500 + + # 2. Test with string status code tucked in response + weird_error = RawHTTPError(MockResponse("500")) + with pytest.raises(litellm.InternalServerError) as exc_info: + exception_type( + model="test-model", + original_exception=weird_error, + custom_llm_provider="hosted_vllm", + completion_kwargs={}, + extra_kwargs={}, + ) + assert exc_info.value.status_code == 500 From 271f902b8689e32492eba30bfebf5d0a7020b5a8 Mon Sep 17 00:00:00 2001 From: Vansh Date: Sat, 3 Oct 2026 23:32:02 +0530 Subject: [PATCH 2/3] style: remove unneeded test commentary per code review --- .../litellm_core_utils/test_exception_mapping_utils.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py index 0370ee967cb..6be669c2fb1 100644 --- a/tests/unit/litellm_core_utils/test_exception_mapping_utils.py +++ b/tests/unit/litellm_core_utils/test_exception_mapping_utils.py @@ -1576,13 +1576,6 @@ def test_guardrail_provider_failure_status_is_still_mapped(): def test_exception_mapping_vllm_nested_status_code(): - """ - Test that when an exception wraps a status_code inside a response object - (or as a string), we safely extract it and return the correct mapped error - (e.g., 500 InternalServerError) rather than swallowing it or returning a - generic APIConnectionError. - """ - class MockResponse: def __init__(self, code): self.status_code = code @@ -1592,7 +1585,6 @@ def test_exception_mapping_vllm_nested_status_code(): self.response = response super().__init__("Mock HTTP Error") - # 1. Test with integer status code tucked in response crash_error = RawHTTPError(MockResponse(500)) with pytest.raises(litellm.InternalServerError) as exc_info: exception_type( @@ -1604,7 +1596,6 @@ def test_exception_mapping_vllm_nested_status_code(): ) assert exc_info.value.status_code == 500 - # 2. Test with string status code tucked in response weird_error = RawHTTPError(MockResponse("500")) with pytest.raises(litellm.InternalServerError) as exc_info: exception_type( From 04e965a0856a2ded33d2e7c8480fc0002c27bd96 Mon Sep 17 00:00:00 2001 From: Vansh Date: Sat, 3 Oct 2026 23:34:59 +0530 Subject: [PATCH 3/3] fix(proxy): add explanatory comment to empty except block to satisfy CodeQL --- .../exception_mapping_utils.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 04640549561..528a4010574 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -4,7 +4,7 @@ import re import traceback from collections.abc import Mapping from types import MappingProxyType -from typing import Final, Protocol, cast +from typing import Final, Optional, Protocol, cast import httpx @@ -419,18 +419,22 @@ def _map_openai_exception( request=_request, litellm_debug_info=extra_information, ) - _status_code = None - try: - if hasattr(original_exception, "status_code") and getattr(original_exception, "status_code", None) is not None: - _status_code = int(original_exception.status_code) - elif ( - hasattr(original_exception, "response") - and hasattr(original_exception.response, "status_code") - and getattr(original_exception.response, "status_code", None) is not None - ): - _status_code = int(original_exception.response.status_code) - except (ValueError, TypeError, AttributeError): - pass + def _extract_status_code() -> Optional[int]: + try: + if hasattr(original_exception, "status_code") and getattr(original_exception, "status_code", None) is not None: + return int(original_exception.status_code) + elif ( + hasattr(original_exception, "response") + and hasattr(original_exception.response, "status_code") + and getattr(original_exception.response, "status_code", None) is not None + ): + return int(original_exception.response.status_code) + except (ValueError, TypeError, AttributeError): + # Ignore parsing errors for malformed status codes and fall back to None + pass + return None + + _status_code: Final = _extract_status_code() if _status_code is not None: if _status_code == 400: