This is an automated email from the ASF dual-hosted git repository.
rusackas 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 5aaf9d337b5 fix(jinja): handle non-JSON body with JSON content-type in
get_form_data (#42196)
5aaf9d337b5 is described below
commit 5aaf9d337b549c2d7e7dbde174d7f32a7d474566
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Tue Jul 28 12:18:08 2026 -0400
fix(jinja): handle non-JSON body with JSON content-type in get_form_data
(#42196)
---
superset/views/utils.py | 24 +++++++++++++++-
tests/unit_tests/views/test_utils.py | 53 ++++++++++++++++++++++++++++++++++++
2 files changed, 76 insertions(+), 1 deletion(-)
diff --git a/superset/views/utils.py b/superset/views/utils.py
index 6fa24539701..804ee7ef262 100644
--- a/superset/views/utils.py
+++ b/superset/views/utils.py
@@ -35,6 +35,7 @@ from flask_appbuilder.security.sqla import models as ab_models
from flask_appbuilder.security.sqla.models import User
from flask_babel import _
from sqlalchemy.exc import NoResultFound
+from werkzeug.exceptions import BadRequest
from superset import appbuilder, dataframe, db, result_set, viz
from superset.common.db_query_status import QueryStatus
@@ -197,6 +198,27 @@ def loads_request_json(request_json_data: str) ->
dict[Any, Any]:
return parsed if isinstance(parsed, dict) else {}
+def get_request_json_body() -> dict[Any, Any]:
+ """Parse the request body as JSON, coercing failures to ``{}``.
+
+ ``request.is_json`` only inspects the Content-Type header, not whether the
+ body is actually parseable JSON. Callers reaching ``get_form_data`` from a
+ non-HTTP-chart-data context (e.g. an MCP tool call rendering
+ ``filter_values()``) can have a request context whose Content-Type claims
+ JSON but whose body isn't a JSON chart-data payload, which makes Werkzeug
+ raise ``BadRequest`` from ``request.get_json()``. A well-formed but
+ non-object JSON body (e.g. ``null``, a scalar, or an array) is coerced to
+ ``{}`` too, since callers treat the result as a mapping.
+ """
+ if not request.is_json:
+ return {}
+ try:
+ data = request.get_json(cache=True)
+ except BadRequest:
+ return {}
+ return data if isinstance(data, dict) else {}
+
+
#: Parameter names `url_for` interprets itself rather than appending to the
#: query string. Request-supplied query keys matching these must never be
#: forwarded as `url_for` kwargs.
@@ -345,7 +367,7 @@ def get_form_data(
form_data: dict[str, Any] = initial_form_data or {}
if has_request_context():
- json_data = request.get_json(cache=True) if request.is_json else {}
+ json_data = get_request_json_body()
# chart data API requests are JSON
first_query = (
diff --git a/tests/unit_tests/views/test_utils.py
b/tests/unit_tests/views/test_utils.py
new file mode 100644
index 00000000000..8da8ebf21f5
--- /dev/null
+++ b/tests/unit_tests/views/test_utils.py
@@ -0,0 +1,53 @@
+# 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.
+"""Tests for superset.views.utils module"""
+
+from flask import current_app
+
+from superset.views.utils import get_form_data
+
+
+def test_get_form_data_handles_non_json_body_with_json_content_type() -> None:
+ """get_form_data returns gracefully when Content-Type claims JSON but the
+ body isn't parseable JSON, instead of letting Werkzeug's BadRequest escape.
+
+ This is the shape of the request context an MCP tool call runs in when a
+ chart/dataset SQL template calls the ``filter_values()`` Jinja macro: the
+ Content-Type header says ``application/json`` but the body is not a JSON
+ chart-data payload.
+ """
+ with current_app.test_request_context(
+ data="not-json-at-all", content_type="application/json"
+ ):
+ form_data, slc = get_form_data()
+
+ assert form_data == {}
+ assert slc is None
+
+
+def test_get_form_data_handles_non_dict_json_body() -> None:
+ """get_form_data coerces a well-formed but non-object JSON body to {}.
+
+ ``request.get_json()`` happily returns a scalar or list for valid JSON
+ that isn't a JSON object (e.g. ``null`` or ``42``). Downstream code treats
+ the parsed body as a mapping, so a non-dict result must not leak through.
+ """
+ with current_app.test_request_context(data="42",
content_type="application/json"):
+ form_data, slc = get_form_data()
+
+ assert form_data == {}
+ assert slc is None