You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/04/27 17:33:08 UTC

(superset) 01/01: fix(datasets): isolate filter state to fix concurrent /dataset race

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

rusackas pushed a commit to branch ce/task-10
in repository https://gitbox.apache.org/repos/asf/superset.git

commit b03642df1d35cf2a7b821619814d6137e98ce8fc
Author: Evan Rusackas <ev...@rusackas.com>
AuthorDate: Mon Apr 27 10:32:02 2026 -0700

    fix(datasets): isolate filter state to fix concurrent /dataset race
    
    Override _handle_filters_args on DatasetRestApi to build a fresh
    Filters instance per request. The FAB default mutates a single
    self._filters shared across requests, which under concurrent traffic
    leaks filters from one request into another (#33828).
    
    Adopts #33895 with review-comment cleanups (drop unused imports, fix
    docstring) and adds a regression test.
    
    Co-Authored-By: Michelle <MIKEX818>
---
 superset/datasets/api.py               | 31 ++++++++++++++++++++++++++-
 tests/unit_tests/datasets/api_tests.py | 38 ++++++++++++++++++++++++++++++++++
 2 files changed, 68 insertions(+), 1 deletion(-)

diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index c44a8ba2e37..4a77b4ebf1a 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -26,7 +26,12 @@ from zipfile import is_zipfile, ZipFile
 from flask import request, Response, send_file
 from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
 from flask_appbuilder.api.schemas import get_item_schema
-from flask_appbuilder.const import API_RESULT_RES_KEY, API_SELECT_COLUMNS_RIS_KEY
+from flask_appbuilder.const import (
+    API_FILTERS_RIS_KEY,
+    API_RESULT_RES_KEY,
+    API_SELECT_COLUMNS_RIS_KEY,
+)
+from flask_appbuilder.models.filters import Filters
 from flask_appbuilder.models.sqla.interface import SQLAInterface
 from flask_babel import ngettext
 from jinja2.exceptions import TemplateSyntaxError
@@ -307,6 +312,30 @@ class DatasetRestApi(BaseSupersetModelRestApi):
     list_outer_default_load = True
     show_outer_default_load = True
 
+    def _handle_filters_args(self, rison_args: dict[str, Any]) -> Filters:
+        """
+        Build a request-scoped ``Filters`` instance from Rison-encoded args.
+
+        Overrides :meth:`flask_appbuilder.api.ModelRestApi._handle_filters_args`,
+        which mutates ``self._filters`` (a single instance shared across requests).
+        Under concurrent traffic that shared state can leak filters from one
+        request into another — e.g. two parallel ``GET /api/v1/dataset/`` calls
+        filtering by different ``table_name`` values can return mixed results.
+
+        Returning a fresh ``Filters`` per call keeps each request isolated.
+
+        :param rison_args: Arguments parsed from the API request's Rison-encoded
+            ``q`` parameter.
+        :returns: A request-scoped ``Filters`` instance joined with the API's
+            base filters.
+        """
+        filters = self.datamodel.get_filters(
+            search_columns=self.search_columns,
+            search_filters=self.search_filters,
+        )
+        filters.rest_add_filters(rison_args.get(API_FILTERS_RIS_KEY, []))
+        return filters.get_joined_filters(self._base_filters)
+
     @expose("/", methods=("POST",))
     @protect()
     @safe
diff --git a/tests/unit_tests/datasets/api_tests.py b/tests/unit_tests/datasets/api_tests.py
index 82e8453c878..88701c76961 100644
--- a/tests/unit_tests/datasets/api_tests.py
+++ b/tests/unit_tests/datasets/api_tests.py
@@ -120,3 +120,41 @@ def test_get_dataset_include_rendered_sql_passes_table_to_template_processor(
 
     assert response.status_code == 200
     mock_get_processor.assert_called_once_with(database=database, table=dataset)
+
+
+def test_handle_filters_args_returns_request_scoped_filters(
+    session: Session,
+    client: Any,
+    full_api_access: None,
+) -> None:
+    """
+    Dataset API: ``_handle_filters_args`` must return a fresh ``Filters``
+    instance per call so concurrent requests don't share filter state.
+
+    Regression test for #33828: under concurrent traffic the FAB default
+    implementation mutates ``self._filters`` (a single shared instance),
+    causing filters from one request to leak into another.
+    """
+    from flask_appbuilder.const import API_FILTERS_RIS_KEY
+
+    from superset.datasets.api import DatasetRestApi
+
+    api = DatasetRestApi()
+    api.datamodel = MagicMock()
+    api.search_columns = ["table_name"]
+    api.search_filters = {}
+    api._base_filters = MagicMock()  # noqa: SLF001
+
+    # Each call should construct a fresh Filters instance via datamodel.get_filters
+    rison_args = {
+        API_FILTERS_RIS_KEY: [{"col": "table_name", "opr": "eq", "value": "a"}],
+    }
+    api._handle_filters_args(rison_args)  # noqa: SLF001
+    api._handle_filters_args(rison_args)  # noqa: SLF001
+
+    assert api.datamodel.get_filters.call_count == 2
+    # Returned object must be the joined-filters result of the *fresh* Filters,
+    # not the shared self._filters attribute.
+    fresh_filters = api.datamodel.get_filters.return_value
+    assert fresh_filters.rest_add_filters.call_count == 2
+    assert fresh_filters.get_joined_filters.call_count == 2