From 73e0fecc7b597e27efb7bcbd082dcb2b739b2c9d Mon Sep 17 00:00:00 2001 From: AMATH <116212274+amathxbt@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:40:37 +0100 Subject: [PATCH 1/3] fix: guard json.loads() calls in JSON tensor parsing to prevent unhandled JSONDecodeError crash --- src/opengradient/client/_conversions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/opengradient/client/_conversions.py b/src/opengradient/client/_conversions.py index 495f663b..18493f20 100644 --- a/src/opengradient/client/_conversions.py +++ b/src/opengradient/client/_conversions.py @@ -156,7 +156,10 @@ def convert_to_model_output(event_data: AttributeDict) -> Dict[str, np.ndarray]: if isinstance(tensor, (AttributeDict, dict)): name = tensor.get("name") value = tensor.get("value") - output_dict[name] = np.array(json.loads(value)) + try: + output_dict[name] = np.array(json.loads(value)) + except (json.JSONDecodeError, TypeError, ValueError) as e: + logging.warning(f"Failed to parse JSON tensor '{name}': {e} (raw value: {value!r})") else: logging.warning(f"Unexpected tensor type: {type(tensor)}") @@ -203,7 +206,10 @@ def convert_array_to_model_output(array_data: List) -> ModelOutput: for tensor in array_data[2]: name = tensor[0] value = tensor[1] - json_data[name] = np.array(json.loads(value)) + try: + json_data[name] = np.array(json.loads(value)) + except (json.JSONDecodeError, TypeError, ValueError) as e: + logging.warning(f"Failed to parse JSON tensor '{name}': {e} (raw value: {value!r})") return ModelOutput( numbers=number_data, @@ -211,3 +217,4 @@ def convert_array_to_model_output(array_data: List) -> ModelOutput: jsons=json_data, is_simulation_result=array_data[3], ) + From 81ce7a8461e8e7d3e27958b84bae6e208d955f6a Mon Sep 17 00:00:00 2001 From: AMATH <116212274+amathxbt@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:48:05 +0100 Subject: [PATCH 2/3] test: update test_convert_array_invalid_json to expect graceful warning instead of crash --- tests/utils_test.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/utils_test.py b/tests/utils_test.py index 082cb8d7..e8203435 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,3 +1,4 @@ +from unittest.mock import patch import json import numpy as np @@ -69,7 +70,12 @@ def test_convert_array_empty(): def test_convert_array_invalid_json(): - """Test handling of invalid JSON data""" + """Test that invalid JSON tensor data is skipped with a warning instead of crashing. + + Previously json.loads() was unguarded and would crash the entire conversion. + After the fix, malformed JSON tensors are skipped and logged as warnings so + all other valid tensors in the same response are still returned. + """ invalid_json_data = [ [], # Empty number tensors [], # Empty string tensors @@ -77,8 +83,18 @@ def test_convert_array_invalid_json(): False, ] - with pytest.raises(json.JSONDecodeError): - utils.convert_array_to_model_output(invalid_json_data) + import logging + with pytest.warns(None) as warning_list: + # Capture logging.warning calls via caplog-style check + with patch("opengradient.client._conversions.logging") as mock_log: + result = utils.convert_array_to_model_output(invalid_json_data) + + # Should not raise; result should have empty jsons dict (bad tensor skipped) + assert isinstance(result, types.ModelOutput) + assert result.jsons == {} + # The warning should have been logged + mock_log.warning.assert_called_once() + assert "invalid_json" in mock_log.warning.call_args[0][0] def test_convert_array_invalid_shape(): From 5e601e8093113eaea8afbf16fadbc1247cd5edf0 Mon Sep 17 00:00:00 2001 From: AMATH <116212274+amathxbt@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:51:03 +0100 Subject: [PATCH 3/3] test: rewrite test_convert_array_invalid_json using caplog fixture (simpler, robust) --- tests/utils_test.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/utils_test.py b/tests/utils_test.py index e8203435..9bba5b53 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,4 +1,3 @@ -from unittest.mock import patch import json import numpy as np @@ -69,13 +68,15 @@ def test_convert_array_empty(): assert result.is_simulation_result is False -def test_convert_array_invalid_json(): +def test_convert_array_invalid_json(caplog): """Test that invalid JSON tensor data is skipped with a warning instead of crashing. Previously json.loads() was unguarded and would crash the entire conversion. After the fix, malformed JSON tensors are skipped and logged as warnings so all other valid tensors in the same response are still returned. """ + import logging + invalid_json_data = [ [], # Empty number tensors [], # Empty string tensors @@ -83,18 +84,15 @@ def test_convert_array_invalid_json(): False, ] - import logging - with pytest.warns(None) as warning_list: - # Capture logging.warning calls via caplog-style check - with patch("opengradient.client._conversions.logging") as mock_log: - result = utils.convert_array_to_model_output(invalid_json_data) + with caplog.at_level(logging.WARNING): + result = utils.convert_array_to_model_output(invalid_json_data) - # Should not raise; result should have empty jsons dict (bad tensor skipped) + # Should not raise; malformed tensor is skipped assert isinstance(result, types.ModelOutput) assert result.jsons == {} - # The warning should have been logged - mock_log.warning.assert_called_once() - assert "invalid_json" in mock_log.warning.call_args[0][0] + + # A warning should have been emitted mentioning the tensor name + assert any("invalid_json" in record.message for record in caplog.records) def test_convert_array_invalid_shape():