mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
fix(safe_json_dumps): snapshot dict items before iterating
safe_dumps runs on request_data and logging_obj.model_call_details, which async guardrail/logging hooks can mutate while serialization is in flight. _serialize iterated obj.items() over the live dict, so a concurrent insert raised "RuntimeError: dictionary changed size during iteration" and killed the request. Wrap items() in list() to take a snapshot before iterating. No behavior change for non-concurrent callers.
This commit is contained in:
parent
d45e9e4d56
commit
6abc171f9a
2 changed files with 26 additions and 1 deletions
|
|
@ -34,7 +34,9 @@ def safe_dumps(data: Any, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH) -> str:
|
|||
result: Union[dict, list, tuple, set, str]
|
||||
if isinstance(obj, dict):
|
||||
result = {}
|
||||
for k, v in obj.items():
|
||||
# Snapshot items() so a concurrent async hook mutating the same
|
||||
# dict can't raise "dictionary changed size during iteration".
|
||||
for k, v in list(obj.items()):
|
||||
if isinstance(k, (str)):
|
||||
result[strip_null_bytes(k)] = _serialize(v, seen, depth + 1)
|
||||
seen.remove(id(obj))
|
||||
|
|
|
|||
|
|
@ -206,3 +206,26 @@ def test_pydantic_base_model():
|
|||
assert len(result["healthy_endpoints"]) == 2
|
||||
assert result["healthy_endpoints"][0]["name"] == "test"
|
||||
assert result["healthy_endpoints"][1] == {"value": 1, "label": "one"}
|
||||
|
||||
|
||||
def test_dict_mutated_during_serialization_does_not_raise():
|
||||
# Regression: safe_dumps runs on request_data / model_call_details, which a
|
||||
# concurrent async hook can mutate while we iterate. Serializing one value
|
||||
# here inserts a new key into the same dict (standing in for that hook). On
|
||||
# the pre-fix code this raised "RuntimeError: dictionary changed size during
|
||||
# iteration"; the list() snapshot must keep iteration stable.
|
||||
class _MutateOnStr:
|
||||
def __init__(self, target):
|
||||
self._target = target
|
||||
|
||||
def __str__(self):
|
||||
self._target["injected_by_concurrent_hook"] = "value"
|
||||
return "mutated"
|
||||
|
||||
data = {}
|
||||
data["trigger"] = _MutateOnStr(data)
|
||||
data["after"] = 1
|
||||
|
||||
result = json.loads(safe_dumps(data))
|
||||
assert result["trigger"] == "mutated"
|
||||
assert result["after"] == 1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue