You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2026/09/28 22:03:18 UTC

(superset) branch master updated: fix(chart/data): guard against non-dict chart.params in get_data (#44770)

This is an automated email from the ASF dual-hosted git repository.

eschutho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 6f6e52ba8b1 fix(chart/data): guard against non-dict chart.params in get_data (#44770)
6f6e52ba8b1 is described below

commit 6f6e52ba8b1201218f6a476a33177141bd492222
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Mon Sep 28 15:03:10 2026 -0700

    fix(chart/data): guard against non-dict chart.params in get_data (#44770)
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
 superset/charts/data/api.py                        |   2 +
 .../unit_tests/charts/data/test_non_dict_params.py | 116 +++++++++++++++++++++
 2 files changed, 118 insertions(+)

diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py
index 3a2e273beda..01e3657d761 100644
--- a/superset/charts/data/api.py
+++ b/superset/charts/data/api.py
@@ -241,6 +241,8 @@ class ChartDataRestApi(ChartRestApi):
             form_data = json.loads(chart.params)
         except (TypeError, json.JSONDecodeError):
             form_data = {}
+        if not isinstance(form_data, dict):
+            form_data = {}
 
         return self._get_data_response(
             command=command,
diff --git a/tests/unit_tests/charts/data/test_non_dict_params.py b/tests/unit_tests/charts/data/test_non_dict_params.py
new file mode 100644
index 00000000000..700c8c8f0d5
--- /dev/null
+++ b/tests/unit_tests/charts/data/test_non_dict_params.py
@@ -0,0 +1,116 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import Any
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from superset.utils import json
+
+
+@pytest.mark.parametrize(
+    "params_value",
+    [
+        pytest.param('"foo"', id="json_string"),
+        pytest.param("[1, 2]", id="json_array"),
+        pytest.param("42", id="json_integer"),
+    ],
+)
+def test_apply_client_processing_crashes_on_non_dict_form_data(
+    params_value: str,
+) -> None:
+    """Prove that apply_client_processing raises AttributeError on non-dict input.
+
+    This is the downstream crash that the isinstance guard in get_data prevents.
+    ``null`` (None) is excluded because it is falsy and caught by the existing
+    ``form_data or {}`` fallback.
+    """
+    from superset.charts.client_processing import apply_client_processing
+
+    parsed = json.loads(params_value)
+    result: dict[str, Any] = {"queries": [], "query_context": MagicMock()}
+    with pytest.raises(AttributeError):
+        apply_client_processing(result, parsed, None)
+
+
+@pytest.mark.parametrize(
+    "params_value",
+    [
+        pytest.param('"foo"', id="json_string"),
+        pytest.param("[1, 2]", id="json_array"),
+        pytest.param("42", id="json_integer"),
+        pytest.param("null", id="json_null"),
+    ],
+)
+def test_get_data_falls_back_for_non_dict_params(
+    params_value: str,
+    full_api_access: None,
+) -> None:
+    """get_data must not pass a non-dict form_data to _get_data_response.
+
+    When chart.params is valid JSON that decodes to something other than a dict,
+    the isinstance guard must coerce it to {} so downstream callers like
+    apply_client_processing never see a non-dict.
+    """
+    from flask import current_app
+
+    from superset.charts.data.api import ChartDataRestApi
+
+    chart = MagicMock()
+    chart.params = params_value
+    chart.query_context = json.dumps(
+        {
+            "datasource": {"id": 1, "type": "table"},
+            "queries": [],
+            "result_format": "json",
+            "result_type": "post_processed",
+        }
+    )
+
+    api = ChartDataRestApi()
+    api.datamodel = MagicMock()
+    api.datamodel.get.return_value = chart
+
+    sentinel = MagicMock(name="response", status_code=200)
+    captured_form_data: list[Any] = []
+
+    def fake_get_data_response(**kwargs: Any) -> Any:
+        captured_form_data.append(kwargs["form_data"])
+        return sentinel
+
+    with current_app.test_request_context("/?type=post_processed"):
+        with (
+            patch.object(api, "_create_query_context_from_form") as mock_create,
+            patch.object(api, "_get_data_response", side_effect=fake_get_data_response),
+            patch.object(api, "_should_run_async", return_value=False),
+            patch("superset.charts.data.api.ChartDataCommand") as mock_cmd_cls,
+            patch("superset.charts.data.api.set_form_data"),
+        ):
+            mock_query_context = MagicMock()
+            mock_create.return_value = mock_query_context
+            mock_cmd = MagicMock()
+            mock_cmd_cls.return_value = mock_cmd
+            mock_cmd.query_context = mock_query_context
+
+            result = api.get_data(pk=1)
+
+    assert result is sentinel
+    assert len(captured_form_data) == 1
+    assert captured_form_data[0] == {}
+    assert isinstance(captured_form_data[0], dict)