mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-15 06:19:29 +00:00
[Fix] Malformed messages returning 500 instead of 400
The existing AttributeError detection in proxy error handling only checked one level deep in the exception chain (__cause__, __context__, original_exception). In practice, the AttributeError from malformed messages gets wrapped in multiple layers (AttributeError -> OpenAIException -> APIConnectionError), so the check never found it. Extracted the check into _has_attribute_error_in_chain() which walks the full exception chain recursively (depth-capped at 10 to prevent infinite loops from circular references). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8882b61296
commit
8ca744036a
@@ -384,6 +384,25 @@ def _get_cost_breakdown_from_logging_obj(
|
||||
return original_cost, discount_amount, margin_total_amount, margin_percent
|
||||
|
||||
|
||||
def _has_attribute_error_in_chain(exc: Exception, _depth: int = 0) -> bool:
|
||||
"""Walk the exception chain to find an AttributeError at any depth.
|
||||
|
||||
Checks __cause__, __context__, and the litellm-specific original_exception
|
||||
attribute recursively. Depth is capped to avoid infinite loops from
|
||||
circular exception references.
|
||||
"""
|
||||
if _depth > 10:
|
||||
return False
|
||||
if isinstance(exc, AttributeError):
|
||||
return True
|
||||
for attr in ("__cause__", "__context__", "original_exception"):
|
||||
inner = getattr(exc, attr, None)
|
||||
if inner is not None and isinstance(inner, BaseException):
|
||||
if _has_attribute_error_in_chain(inner, _depth + 1):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ProxyBaseLLMRequestProcessing:
|
||||
def __init__(self, data: dict):
|
||||
self.data = data
|
||||
@@ -1281,23 +1300,11 @@ class ProxyBaseLLMRequestProcessing:
|
||||
detail={"error": error_text},
|
||||
)
|
||||
error_msg = f"{str(e)}"
|
||||
# Check for AttributeError in various places:
|
||||
# 1. Direct AttributeError (already handled above)
|
||||
# 2. In underlying exception (__cause__, __context__, original_exception)
|
||||
has_attribute_error = (
|
||||
(
|
||||
isinstance(e, Exception)
|
||||
and isinstance(getattr(e, "__cause__", None), AttributeError)
|
||||
)
|
||||
or (
|
||||
isinstance(e, Exception)
|
||||
and isinstance(getattr(e, "__context__", None), AttributeError)
|
||||
)
|
||||
or (
|
||||
isinstance(e, Exception)
|
||||
and isinstance(getattr(e, "original_exception", None), AttributeError)
|
||||
)
|
||||
)
|
||||
# Check for AttributeError in the exception chain.
|
||||
# The AttributeError may be wrapped in multiple layers
|
||||
# (e.g. AttributeError -> OpenAIException -> APIConnectionError),
|
||||
# so walk __cause__, __context__, and original_exception recursively.
|
||||
has_attribute_error = _has_attribute_error_in_chain(e)
|
||||
|
||||
if has_attribute_error:
|
||||
raise ProxyException(
|
||||
|
||||
@@ -15,6 +15,7 @@ from litellm.proxy.common_request_processing import (
|
||||
ProxyConfig,
|
||||
_extract_error_from_sse_chunk,
|
||||
_get_cost_breakdown_from_logging_obj,
|
||||
_has_attribute_error_in_chain,
|
||||
_is_azure_model_router_request,
|
||||
_override_openai_response_model,
|
||||
_parse_event_data_for_error,
|
||||
@@ -1701,3 +1702,50 @@ class TestDDSpanTaggerTagRequest:
|
||||
)
|
||||
|
||||
mock_set_tag.assert_called_once_with("litellm.requested_model", "claude-3-5-sonnet")
|
||||
|
||||
|
||||
class TestHasAttributeErrorInChain:
|
||||
"""Tests for _has_attribute_error_in_chain helper."""
|
||||
|
||||
def test_direct_attribute_error(self):
|
||||
exc = AttributeError("'str' object has no attribute 'get'")
|
||||
assert _has_attribute_error_in_chain(exc) is True
|
||||
|
||||
def test_no_attribute_error(self):
|
||||
exc = ValueError("some other error")
|
||||
assert _has_attribute_error_in_chain(exc) is False
|
||||
|
||||
def test_attribute_error_in_cause(self):
|
||||
inner = AttributeError("bad attribute")
|
||||
outer = RuntimeError("wrapper")
|
||||
outer.__cause__ = inner
|
||||
assert _has_attribute_error_in_chain(outer) is True
|
||||
|
||||
def test_attribute_error_in_context(self):
|
||||
inner = AttributeError("bad attribute")
|
||||
outer = RuntimeError("wrapper")
|
||||
outer.__context__ = inner
|
||||
assert _has_attribute_error_in_chain(outer) is True
|
||||
|
||||
def test_attribute_error_in_original_exception(self):
|
||||
inner = AttributeError("bad attribute")
|
||||
outer = RuntimeError("wrapper")
|
||||
outer.original_exception = inner # type: ignore
|
||||
assert _has_attribute_error_in_chain(outer) is True
|
||||
|
||||
def test_attribute_error_nested_two_levels(self):
|
||||
"""Simulates the real failure: AttributeError -> OpenAIException -> APIConnectionError."""
|
||||
attr_err = AttributeError("'str' object has no attribute 'get'")
|
||||
mid = Exception("OpenAIException wrapper")
|
||||
mid.__context__ = attr_err
|
||||
outer = Exception("APIConnectionError wrapper")
|
||||
outer.__context__ = mid
|
||||
assert _has_attribute_error_in_chain(outer) is True
|
||||
|
||||
def test_depth_limit_prevents_infinite_loop(self):
|
||||
"""Ensure circular references don't cause infinite recursion."""
|
||||
exc_a = RuntimeError("a")
|
||||
exc_b = RuntimeError("b")
|
||||
exc_a.__context__ = exc_b
|
||||
exc_b.__context__ = exc_a # circular
|
||||
assert _has_attribute_error_in_chain(exc_a) is False
|
||||
|
||||
Reference in New Issue
Block a user