Files
litellm/tests/test_litellm/test_exception_mapping_request_attribute.py
T
2026-04-17 13:02:59 -07:00

270 lines
9.1 KiB
Python

"""
Unit tests for the exception mapping request attribute handling fix.
This test verifies the fix for PR #15013 where getattr(original_exception, "request", None)
is used instead of original_exception.request to handle cases where exceptions don't have
a request attribute.
The key fix is that accessing original_exception.request directly would raise AttributeError
if the exception doesn't have a request attribute, but getattr(original_exception, "request", None)
safely returns None instead.
PR #15013 fixed 12 locations in exception_mapping_utils.py where direct access to .request
was replaced with getattr() calls:
- Line 1501: Cohere exception mapping
- Line 1574: HuggingFace exception mapping
- Line 1635: AI21 exception mapping
- Line 1660: NLP Cloud exception mapping
- Line 1720: NLP Cloud exception mapping (another case)
- Line 1740: NLP Cloud exception mapping (another case)
- Line 1851: Together AI exception mapping
- Line 1954: VLLM exception mapping
- Line 2209: Generic provider exception mapping
- Line 2244: Generic provider exception mapping (fallback)
- OpenRouter exception mapping (multiple locations)
This test ensures that none of these code paths will raise AttributeError when an exception
object doesn't have a request attribute, which was the root cause of the bug.
"""
import pytest
import httpx
from unittest.mock import patch
import litellm
from litellm.litellm_core_utils.exception_mapping_utils import exception_type
from litellm.exceptions import APIError, APIConnectionError
class MockExceptionWithoutRequest:
"""Mock exception that does NOT have a request attribute."""
def __init__(self, status_code=500, message="Test error"):
self.status_code = status_code
self.message = message
# Intentionally no request attribute
def test_exception_mapping_request_attribute_fix():
"""
Test the core fix: getattr(original_exception, "request", None) should not raise AttributeError
even when the exception doesn't have a request attribute.
This is the main test for PR #15013.
"""
# Test case 1: Exception without request attribute should not cause AttributeError
mock_exception = MockExceptionWithoutRequest(
status_code=500, message="Test error without request attribute"
)
# The test is that this should NOT raise an AttributeError about missing 'request'
try:
exception_type(
model="test-model",
custom_llm_provider="cohere", # Using cohere as it's one of the affected providers
original_exception=mock_exception,
completion_kwargs={},
extra_kwargs={},
)
# We expect some exception to be raised (the mapped exception), but not AttributeError
except AttributeError as e:
if "'request'" in str(e):
pytest.fail(
f"The fix failed: Should not raise AttributeError about missing 'request' attribute: {e}"
)
else:
# If it's a different AttributeError, re-raise it
raise
except Exception:
# Any other exception is fine - we just want to ensure no AttributeError about 'request'
pass
def test_request_attribute_safety_with_getattr():
"""
Test that the getattr approach works correctly for both cases:
1. When request attribute exists
2. When request attribute doesn't exist
"""
# Case 1: Exception with request attribute
class MockExceptionWithRequest:
def __init__(self):
self.status_code = 500
self.message = "Test error"
self.request = httpx.Request(method="POST", url="https://api.example.com")
exception_with_request = MockExceptionWithRequest()
request_value = getattr(exception_with_request, "request", None)
assert request_value is not None
assert isinstance(request_value, httpx.Request)
# Case 2: Exception without request attribute
exception_without_request = MockExceptionWithoutRequest()
request_value = getattr(exception_without_request, "request", None)
assert request_value is None # Should be None, not raise AttributeError
def test_providers_affected_by_fix():
"""
Test that the specific providers mentioned in the PR changes handle missing request attributes correctly.
The PR changes affected these provider-specific code paths:
- cohere: line 1501
- huggingface: line 1574
- ai21: line 1635
- nlp_cloud: lines 1660, 1720, 1740
- together_ai: line 1851
- vllm: line 1954
- generic providers: lines 2209, 2244
"""
providers_to_test = ["cohere", "ai21", "together_ai", "vllm"]
for provider in providers_to_test:
mock_exception = MockExceptionWithoutRequest(
status_code=500, message=f"Test error for {provider}"
)
# The key test: this should not raise AttributeError about missing 'request'
try:
exception_type(
model=f"{provider}-test-model",
custom_llm_provider=provider,
original_exception=mock_exception,
completion_kwargs={},
extra_kwargs={},
)
except AttributeError as e:
if "'request'" in str(e):
pytest.fail(
f"Provider {provider} failed: Should not raise AttributeError about missing 'request' attribute: {e}"
)
except Exception:
# Any other exception is expected and fine
pass
def test_huggingface_specific_case():
"""
Test HuggingFace specific case which has its own handling logic.
"""
mock_exception = MockExceptionWithoutRequest(
status_code=400, message="length limit exceeded"
)
try:
exception_type(
model="huggingface-model",
custom_llm_provider="huggingface",
original_exception=mock_exception,
completion_kwargs={},
extra_kwargs={},
)
except AttributeError as e:
if "'request'" in str(e):
pytest.fail(
f"HuggingFace exception handling failed: Should not raise AttributeError about missing 'request' attribute: {e}"
)
except litellm.ContextWindowExceededError:
# Expected for "length limit exceeded" message
pass
except Exception:
# Other exceptions are fine
pass
def test_nlp_cloud_specific_case():
"""
Test NLP Cloud specific case which had multiple lines changed in the PR.
"""
mock_exception = MockExceptionWithoutRequest(
status_code=504, message="Gateway timeout"
)
try:
exception_type(
model="nlp-cloud-model",
custom_llm_provider="nlp_cloud",
original_exception=mock_exception,
completion_kwargs={},
extra_kwargs={},
)
except AttributeError as e:
if "'request'" in str(e):
pytest.fail(
f"NLP Cloud exception handling failed: Should not raise AttributeError about missing 'request' attribute: {e}"
)
except Exception:
# Any other exception is expected
pass
def test_generic_fallback_case():
"""
Test the generic fallback case at the end of exception_type function.
This tests the changes in lines 2209 and 2244 of the PR.
"""
mock_exception = MockExceptionWithoutRequest(
status_code=500, message="Generic error"
)
try:
exception_type(
model="unknown-model",
custom_llm_provider="unknown_provider",
original_exception=mock_exception,
completion_kwargs={},
extra_kwargs={},
)
except AttributeError as e:
if "'request'" in str(e):
pytest.fail(
f"Generic fallback failed: Should not raise AttributeError about missing 'request' attribute: {e}"
)
except APIConnectionError:
# Expected for generic fallback
pass
except Exception:
# Other exceptions might be fine too
pass
def test_openrouter_specific_case():
"""
Test OpenRouter which also uses the request attribute in exception mapping.
"""
mock_exception = MockExceptionWithoutRequest(
status_code=500, message="OpenRouter error"
)
try:
exception_type(
model="openrouter-model",
custom_llm_provider="openrouter",
original_exception=mock_exception,
completion_kwargs={},
extra_kwargs={},
)
except AttributeError as e:
if "'request'" in str(e):
pytest.fail(
f"OpenRouter exception handling failed: Should not raise AttributeError about missing 'request' attribute: {e}"
)
except Exception:
# Other exceptions are expected
pass
if __name__ == "__main__":
# Run tests for manual verification
test_exception_mapping_request_attribute_fix()
test_request_attribute_safety_with_getattr()
test_providers_affected_by_fix()
test_huggingface_specific_case()
test_nlp_cloud_specific_case()
test_generic_fallback_case()
test_openrouter_specific_case()
print("All tests passed!")