From 4a24fb5a0e66ddb137eee683b4f087e86b8611f3 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Thu, 6 Aug 2026 16:47:15 +0000 Subject: [PATCH] fix(sql-lab): raise SupersetTemplateException instead of raw UndefinedError in SQLExecutor.execute_async _render_sql_template() called process_template() with no try/except, so a Jinja UndefinedError for an undefined variable not called as a function would leak raw past execute_async() (execute() already caught it via its broad except Exception). Wrap the call and re-raise as SupersetTemplateException, consistent with how process_template() itself already handles this failure mode elsewhere. --- superset/sql/execution/executor.py | 8 +++- .../unit_tests/sql/execution/test_executor.py | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/superset/sql/execution/executor.py b/superset/sql/execution/executor.py index 4c29dfdaa0df..9003169c651d 100644 --- a/superset/sql/execution/executor.py +++ b/superset/sql/execution/executor.py @@ -66,6 +66,7 @@ from flask import current_app as app, g, has_app_context from flask_babel import gettext as __ +from jinja2.exceptions import TemplateError from superset import db from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -75,6 +76,7 @@ SupersetErrorException, SupersetParseError, SupersetSecurityException, + SupersetTemplateException, SupersetTimeoutException, ) from superset.extensions import cache_manager @@ -751,6 +753,7 @@ def _render_sql_template( :param sql: SQL string potentially containing Jinja2 templates :param template_params: Parameters to pass to the template :returns: Rendered SQL string + :raises SupersetTemplateException: if the template fails to render """ if template_params is None: return sql @@ -758,7 +761,10 @@ def _render_sql_template( from superset.jinja_context import get_template_processor tp = get_template_processor(database=self.database) - return tp.process_template(sql, **template_params) + try: + return tp.process_template(sql, **template_params) + except TemplateError as ex: + raise SupersetTemplateException(str(ex)) from ex def _apply_limit_to_script(self, script: SQLScript, opts: QueryOptions) -> None: """ diff --git a/tests/unit_tests/sql/execution/test_executor.py b/tests/unit_tests/sql/execution/test_executor.py index 445156fd3726..dd93bb596a81 100644 --- a/tests/unit_tests/sql/execution/test_executor.py +++ b/tests/unit_tests/sql/execution/test_executor.py @@ -44,6 +44,7 @@ ) from superset.models.core import Database +from tests.unit_tests.conftest import with_feature_flags # Note: database, database_with_dml, mock_db_session fixtures and # mock_query_execution helper are imported from conftest.py @@ -789,6 +790,42 @@ def test_execute_async_dml_without_permission_raises( database.execute_async("INSERT INTO users (name) VALUES ('test')") +@with_feature_flags(ENABLE_TEMPLATE_PROCESSING=True) +def test_execute_async_undefined_template_var_raises_superset_template_exception( + mocker: MockerFixture, database: Database, app_context: None +) -> None: + """A Jinja template referencing an undefined variable (not called as a + function) must not leak a raw ``jinja2.exceptions.UndefinedError`` out of + ``execute_async`` - it should surface as ``SupersetTemplateException``.""" + from superset.exceptions import SupersetTemplateException + + mocker.patch.dict( + current_app.config, {"SQL_QUERY_MUTATOR": None, "SQLLAB_TIMEOUT": 30} + ) + + options = QueryOptions(template_params={"foo": "bar"}) + + with pytest.raises(SupersetTemplateException): + database.execute_async("SELECT {{ missing_var[0] }}", options=options) + + +@with_feature_flags(ENABLE_TEMPLATE_PROCESSING=True) +def test_execute_sync_undefined_template_var_returns_failed_result( + mocker: MockerFixture, database: Database, app_context: None +) -> None: + """The sync ``execute`` path's broad ``except Exception`` still catches the + template rendering failure and returns a FAILED ``QueryResult``, unchanged + by the ``_render_sql_template`` fix.""" + mocker.patch.dict( + current_app.config, {"SQL_QUERY_MUTATOR": None, "SQLLAB_TIMEOUT": 30} + ) + + options = QueryOptions(template_params={"foo": "bar"}) + result = database.execute("SELECT {{ missing_var[0] }}", options=options) + + assert result.status == QueryStatus.FAILED + + def test_async_handle_get_status( mocker: MockerFixture, database: Database,