[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:
yuneng-jiang
2026-03-12 23:01:25 -07:00
co-authored by Claude Opus 4.6
parent 8882b61296
commit 8ca744036a
2 changed files with 72 additions and 17 deletions
+24 -17
View File
@@ -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