diff --git a/superset/sqllab/api.py b/superset/sqllab/api.py index 5b6d585f9c7..62e5a683cb3 100644 --- a/superset/sqllab/api.py +++ b/superset/sqllab/api.py @@ -23,6 +23,7 @@ from flask import current_app as app, request, Response from flask_appbuilder import permission_name from flask_appbuilder.api import expose, protect, rison as parse_rison, safe from flask_appbuilder.models.sqla.interface import SQLAInterface +from jinja2.exceptions import TemplateError from marshmallow import ValidationError from werkzeug.utils import secure_filename @@ -37,6 +38,8 @@ from superset.commands.sql_lab.streaming_export_command import ( from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.daos.database import DatabaseDAO from superset.daos.query import QueryDAO +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType +from superset.exceptions import SupersetErrorException from superset.extensions import event_logger, security_manager from superset.jinja_context import get_template_processor from superset.models.sql_lab import Query @@ -266,6 +269,15 @@ class SqlLabRestApi(BaseSupersetApi): "Invalid template parameter %s. Skipping processing", str(template_params), ) + except TemplateError as ex: + raise SupersetErrorException( + SupersetError( + message=str(ex), + error_type=SupersetErrorType.GENERIC_COMMAND_ERROR, + level=ErrorLevel.ERROR, + ), + status=400, + ) from ex result = SQLScript(sql, model.get("engine", database_engine)).format() return self.response(200, result=result) diff --git a/tests/integration_tests/sql_lab/api_tests.py b/tests/integration_tests/sql_lab/api_tests.py index 4329946fe89..4527112000c 100644 --- a/tests/integration_tests/sql_lab/api_tests.py +++ b/tests/integration_tests/sql_lab/api_tests.py @@ -336,6 +336,29 @@ class TestSqlLabApi(SupersetTestCase): assert "{{tbl}}" not in resp_data["result"] assert resp_data["result"] == expected + def test_format_sql_request_with_undefined_jinja_attribute(self): + self.login(ADMIN_USERNAME) + example_db = get_example_database() + + # Attribute access on an undefined Jinja variable (``tbl`` is not in the + # template params) raises a jinja2 ``UndefinedError`` from within + # ``process_template``. This should surface as a typed 400, not an opaque + # 500 from the global exception handler. A non-empty ``template_params`` + # dict is required so the endpoint actually invokes ``process_template``. + data = { + "sql": "select * from {{ tbl.name }}", + "database_id": example_db.id, + "template_params": json.dumps({"unrelated": "value"}), + } + rv = self.client.post( + "/api/v1/sqllab/format_sql/", + json=data, + ) + resp_data = json.loads(rv.data.decode("utf-8")) + assert rv.status_code == 400 + assert "tbl" in resp_data["errors"][0]["message"] + assert "undefined" in resp_data["errors"][0]["message"] + @mock.patch("superset.commands.sql_lab.results.results_backend_use_msgpack", False) def test_execute_required_params(self): self.login(ADMIN_USERNAME)