From 3ed3b861fa495fc32bffef989c05eea9075b2b3b Mon Sep 17 00:00:00 2001 From: yryzhan Date: Wed, 20 May 2026 15:25:25 +0200 Subject: [PATCH] fix(core): snapshot dict in safe_json_dumps to prevent concurrent mutation crash _serialize iterates obj.items() directly, which raises RuntimeError if another thread mutates the dict concurrently. Snapshot via list(obj.items()) eliminates the race. --- litellm/litellm_core_utils/safe_json_dumps.py | 2 +- .../test_safe_json_dumps.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/safe_json_dumps.py b/litellm/litellm_core_utils/safe_json_dumps.py index 051aa2f27a5..afb6c018a2d 100644 --- a/litellm/litellm_core_utils/safe_json_dumps.py +++ b/litellm/litellm_core_utils/safe_json_dumps.py @@ -26,7 +26,7 @@ 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(): + for k, v in list(obj.items()): if isinstance(k, (str)): result[k] = _serialize(v, seen, depth + 1) seen.remove(id(obj)) diff --git a/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py b/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py index c71a229cca5..26157e003f1 100644 --- a/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py +++ b/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py @@ -1,6 +1,7 @@ import json import os import sys +import threading import pytest @@ -172,3 +173,32 @@ 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_no_runtime_error_on_concurrent_dict_mutation(): + """safe_dumps must not raise RuntimeError when another thread mutates the dict.""" + data = {f"key_{i}": f"value_{i}" for i in range(200)} + barrier = threading.Barrier(2, timeout=5) + errors = [] + + def mutator(): + barrier.wait() + for i in range(200, 400): + data[f"key_{i}"] = f"value_{i}" + + def serializer(): + barrier.wait() + try: + safe_dumps(data) + except RuntimeError as e: + if "dictionary changed size during iteration" in str(e): + errors.append(e) + + t1 = threading.Thread(target=mutator) + t2 = threading.Thread(target=serializer) + t1.start() + t2.start() + t1.join(timeout=5) + t2.join(timeout=5) + + assert not errors, "safe_dumps raised RuntimeError on concurrent mutation"