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

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 11bf87404c2 Restrict the second render pass in 
SqlToSlackWebhookOperator (#71402)
11bf87404c2 is described below

commit 11bf87404c24f11821617992afe3e12c9457efa4
Author: JelyFishhhhhh <[email protected]>
AuthorDate: Tue Oct 6 00:09:20 2026 +0800

    Restrict the second render pass in SqlToSlackWebhookOperator (#71402)
    
    * Restrict the second render pass in SqlToSlackWebhookOperator
    
    The operator renders in two passes because slack_message references
    results_df, which does not exist until the query has run. The first pass
    renders every templated field except slack_message; the second is meant to
    fill in that one deferred field.
    
    The second pass rendered self.template_fields instead, so sql - already
    rendered by the first pass - was compiled again with its own rendered output
    as the template source. Jinja syntax arriving inside a context value was
    therefore evaluated on the second pass rather than staying literal.
    
    Restrict the second pass to slack_message. The first pass is unchanged and
    slack_message still renders exactly as before.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    * Render only slack_message when sending, also for mapped tasks
    
    A mapped task's first render bypasses render_template_fields and leaves
    times_rendered at 0, so sending re-rendered the already-rendered sql.
    Render the deferred slack_message field directly instead of relying on
    the counter.
    
    Generated-by: Claude Opus 5
    
    * Render slack_message once from its original template
    
    A mapped task's first render also renders slack_message, so rendering
    the attribute again at send time evaluated Jinja that arrived in a
    context value. Keep the template given to the constructor and render
    that, once, when sending.
    
    Generated-by: Claude Opus 5
    
    ---------
    
    Co-authored-by: JelyF1shhhhhh <[email protected]>
    Co-authored-by: Claude Opus 5 <[email protected]>
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .../slack/transfers/sql_to_slack_webhook.py        | 28 +++++++---
 .../slack/transfers/test_sql_to_slack_webhook.py   | 61 ++++++++++++++++++++++
 2 files changed, 82 insertions(+), 7 deletions(-)

diff --git 
a/providers/slack/src/airflow/providers/slack/transfers/sql_to_slack_webhook.py 
b/providers/slack/src/airflow/providers/slack/transfers/sql_to_slack_webhook.py
index 1892a9293fe..04b6bb7c40b 100644
--- 
a/providers/slack/src/airflow/providers/slack/transfers/sql_to_slack_webhook.py
+++ 
b/providers/slack/src/airflow/providers/slack/transfers/sql_to_slack_webhook.py
@@ -97,13 +97,21 @@ class SqlToSlackWebhookOperator(BaseSqlToSlackOperator):
         self.slack_webhook_conn_id = slack_webhook_conn_id
         self.slack_channel = slack_channel
         self.slack_message = slack_message
+        # Rendered once, at send time, from this original text. A mapped 
task's first render also
+        # renders slack_message, and rendering that output again would 
evaluate any Jinja that arrived
+        # in a context value (``dag_run.conf``, a param).
+        self._slack_message_template = slack_message
         self.results_df_name = results_df_name
         self.kwargs = kwargs
 
     def _render_and_send_slack_message(self, context, df) -> None:
-        # Put the dataframe into the context and render the JINJA template 
fields
+        # Render only slack_message, without going through 
render_template_fields: a mapped task's
+        # first render never increments times_rendered, so relying on that 
counter would render the
+        # already-rendered fields a second time.
         context[self.results_df_name] = df
-        self.render_template_fields(context)
+        self.slack_message = self.render_template(
+            self._slack_message_template, context, self._get_jinja_env()
+        )
 
         slack_hook = self._get_slack_hook()
         self.log.info("Sending slack message: %s", self.slack_message)
@@ -123,16 +131,22 @@ class SqlToSlackWebhookOperator(BaseSqlToSlackOperator):
         if self.times_rendered == 0:
             fields_to_render: Iterable[str] = (x for x in self.template_fields 
if x != "slack_message")
         else:
-            fields_to_render = self.template_fields
+            # Only the deferred field. Re-rendering a field that the first 
pass already rendered
+            # feeds its own output back in as template source, so any Jinja 
syntax that arrived in
+            # a context value (``dag_run.conf``, a param) would be evaluated 
on this second pass.
+            fields_to_render = ("slack_message",)
+
+        self._do_render_template_fields(
+            self, fields_to_render, context, self._get_jinja_env(jinja_env), 
set()
+        )
+        self.times_rendered += 1
 
+    def _get_jinja_env(self, jinja_env=None):
         if not jinja_env:
             jinja_env = self.get_template_env()
-
         # Add the tabulate library into the JINJA environment
         jinja_env.filters["tabulate"] = tabulate
-
-        self._do_render_template_fields(self, fields_to_render, context, 
jinja_env, set())
-        self.times_rendered += 1
+        return jinja_env
 
     def execute(self, context: Context) -> None:
         if not isinstance(self.sql, str):
diff --git 
a/providers/slack/tests/unit/slack/transfers/test_sql_to_slack_webhook.py 
b/providers/slack/tests/unit/slack/transfers/test_sql_to_slack_webhook.py
index beb91bf255d..49eef22bfd9 100644
--- a/providers/slack/tests/unit/slack/transfers/test_sql_to_slack_webhook.py
+++ b/providers/slack/tests/unit/slack/transfers/test_sql_to_slack_webhook.py
@@ -133,6 +133,67 @@ class TestSqlToSlackWebhookOperator:
             channel="#test",
         )
 
+    def test_second_render_leaves_already_rendered_fields_alone(self, 
mocked_hook):
+        """The second render pass must touch only ``slack_message``.
+
+        The first pass renders every other templated field; re-rendering them 
afterwards would feed
+        each field's own output back in as template source, so Jinja syntax 
that arrived inside a
+        context value would be evaluated on the second pass instead of staying 
literal.
+        """
+        mock_dbapi_hook = mock.Mock()
+        mock_dbapi_hook.return_value.get_df.return_value = pd.DataFrame({"a": 
"1"}, index=[0])
+
+        sql_to_slack_operator = self._construct_operator(
+            sql_conn_id="snowflake_connection",
+            slack_webhook_conn_id="slack_connection",
+            slack_message="message: {{ results_df }}",
+            slack_channel="#test",
+            sql="SELECT '{{ ds }}'",
+        )
+        sql_to_slack_operator._get_hook = mock_dbapi_hook
+
+        # ``ds`` resolves to a value that itself contains Jinja - the shape a 
triggering user can
+        # produce through ``dag_run.conf``. It must survive as literal text.
+        context = {"ds": "{{ leaked }}", "leaked": "SECRET"}
+
+        sql_to_slack_operator.render_template_fields(context)
+        assert sql_to_slack_operator.sql == "SELECT '{{ leaked }}'"
+
+        sql_to_slack_operator.execute(context)
+        assert sql_to_slack_operator.sql == "SELECT '{{ leaked }}'"
+
+    def test_send_renders_each_field_once_after_mapped_first_render(self, 
mocked_hook):
+        """A mapped task's first render bypasses ``render_template_fields``: 
it renders every template
+        field, ``slack_message`` included, and leaves ``times_rendered`` at 0. 
Sending must not render
+        any field a second time, or Jinja that arrived in a context value 
would be evaluated."""
+        mock_dbapi_hook = mock.Mock()
+        mock_dbapi_hook.return_value.get_df.return_value = pd.DataFrame({"a": 
"1"}, index=[0])
+
+        sql_to_slack_operator = self._construct_operator(
+            sql_conn_id="snowflake_connection",
+            slack_webhook_conn_id="slack_connection",
+            slack_message="message: {{ ds }}",
+            slack_channel="#test",
+            sql="SELECT '{{ ds }}'",
+        )
+        sql_to_slack_operator._get_hook = mock_dbapi_hook
+        context = {"ds": "{{ leaked }}", "leaked": "SECRET"}
+
+        # What MappedOperator.render_template_fields does for the unmapped 
task.
+        sql_to_slack_operator._do_render_template_fields(
+            sql_to_slack_operator,
+            sql_to_slack_operator.template_fields,
+            context,
+            sql_to_slack_operator.get_template_env(),
+            set(),
+        )
+        assert sql_to_slack_operator.times_rendered == 0
+
+        sql_to_slack_operator.execute(context)
+
+        assert sql_to_slack_operator.sql == "SELECT '{{ leaked }}'"
+        assert sql_to_slack_operator.slack_message == "message: {{ leaked }}"
+
     @pytest.mark.parametrize(
         ("slack_webhook_conn_id", "warning_expected", "expected_conn_id"),
         [

Reply via email to