From 89919a1c96a12c0b4bac15ff24eded415a42ec70 Mon Sep 17 00:00:00 2001 From: Joe Li Date: Fri, 15 May 2026 09:26:37 -0700 Subject: [PATCH] fix(subdirectory): avoid explore redirect loop when form_data has no datasource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI 25898464041 (Python-Integration, postgres + mysql) failed `test_slices` and `test_slice_id_is_always_logged_correctly_on_web_request` with werkzeug.test.ClientRedirectError: Loop detected: A 302 redirect to /explore/?slice_id=216&form_data={"slice_id": 216} was already made. `Slice.slice_url` builds /explore/?slice_id=X&form_data={"slice_id": X}, i.e. no datasource. The ExploreView.root redirect added in d8335c0e1b fires unconditionally when form_data is present, but Superset.get_redirect_url() only rewrites the URL when parsed_form_data["datasource"] exists (the form_data → form_data_key caching path is gated on a datasource). Without one, the redirect target equals the source and the test client follows itself in circles. Fix: pre-parse form_data and only delegate to get_redirect_url when a datasource is present; otherwise fall through to render_app_template. Co-Authored-By: Claude Opus 4.7 (1M context) --- superset/views/explore.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/superset/views/explore.py b/superset/views/explore.py index f8b41bd53d6..3023052539b 100644 --- a/superset/views/explore.py +++ b/superset/views/explore.py @@ -21,6 +21,7 @@ from flask_appbuilder.security.decorators import has_access from superset import event_logger from superset.superset_typing import FlaskResponse +from superset.views.utils import loads_request_json from .base import BaseSupersetView @@ -37,11 +38,20 @@ class ExploreView(BaseSupersetView): # After `Superset.route_base = ""`, both `Superset.explore` and this # view register at `/explore/`; this view wins. Preserve the legacy # form_data → form_data_key cache-and-redirect contract here so - # callers passing `?form_data=...` still get the short cache-key URL. - if request.args.get("form_data"): - from superset.views.core import Superset # avoid circular import + # callers passing `?form_data=...` with a datasource still get the + # short cache-key URL. Form_data without a datasource (e.g. legacy + # `slice_url` payloads carrying only `slice_id`) cannot be cached, + # so `get_redirect_url` would return the same URL — falling back to + # SPA rendering avoids a 302 loop. + if request_form_data := request.args.get("form_data"): + try: + parsed_form_data = loads_request_json(request_form_data) + except ValueError: + parsed_form_data = {} + if parsed_form_data.get("datasource"): + from superset.views.core import Superset # avoid circular import - return redirect(Superset.get_redirect_url()) + return redirect(Superset.get_redirect_url()) return super().render_app_template()