This is an automated email from the ASF dual-hosted git repository. kaxilnaik pushed a commit to branch v3-1-test in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 09560358e062f238e89018dc45dba44fb382b747 Author: Kaxil Naik <[email protected]> AuthorDate: Mon Sep 15 22:27:33 2025 +0100 Fix task log URL generation with various `base_url` formats (#55699) The `RuntimeTaskInstance.log_url` property incorrectly concatenated base_url with the path, causing malformed URLs when base_url didn't end with a slash. Fixes #54247 Closes https://github.com/apache/airflow/pull/54248 Closes https://github.com/apache/airflow/pull/53884 (cherry picked from commit 3e1638250fb8f58a399ad8c5946dbb77a079f63d) --- airflow-core/src/airflow/models/taskinstance.py | 2 +- .../src/airflow/sdk/execution_time/task_runner.py | 19 +--------------- .../task_sdk/execution_time/test_task_runner.py | 25 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/airflow-core/src/airflow/models/taskinstance.py b/airflow-core/src/airflow/models/taskinstance.py index 36193d077f3..72a5ab0b24b 100644 --- a/airflow-core/src/airflow/models/taskinstance.py +++ b/airflow-core/src/airflow/models/taskinstance.py @@ -622,7 +622,7 @@ class TaskInstance(Base, LoggingMixin): base_url = conf.get("api", "base_url", fallback="http://localhost:8080/") map_index = f"/mapped/{self.map_index}" if self.map_index >= 0 else "" try_number = f"?try_number={self.try_number}" if self.try_number > 0 else "" - _log_uri = f"{base_url}dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" + _log_uri = f"{base_url.rstrip('/')}/dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" return _log_uri diff --git a/task-sdk/src/airflow/sdk/execution_time/task_runner.py b/task-sdk/src/airflow/sdk/execution_time/task_runner.py index 02d5eb23996..bb92ff00154 100644 --- a/task-sdk/src/airflow/sdk/execution_time/task_runner.py +++ b/task-sdk/src/airflow/sdk/execution_time/task_runner.py @@ -562,7 +562,7 @@ class RuntimeTaskInstance(TaskInstance): try_number = ( f"?try_number={try_number_value}" if try_number_value is not None and try_number_value > 0 else "" ) - _log_uri = f"{base_url}dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" + _log_uri = f"{base_url.rstrip('/')}/dags/{self.dag_id}/runs/{run_id}/tasks/{self.task_id}{map_index}{try_number}" return _log_uri @property @@ -599,23 +599,6 @@ def _xcom_push_to_db(ti: RuntimeTaskInstance, key: str, value: Any) -> None: ) -def get_log_url_from_ti(ti: RuntimeTaskInstance) -> str: - from urllib.parse import quote - - from airflow.configuration import conf - - run_id = quote(ti.run_id) - base_url = conf.get("api", "base_url", fallback="http://localhost:8080/") - map_index_value = getattr(ti, "map_index", -1) - map_index = f"/mapped/{map_index_value}" if map_index_value is not None and map_index_value >= 0 else "" - try_number_value = getattr(ti, "try_number", 0) - try_number = ( - f"?try_number={try_number_value}" if try_number_value is not None and try_number_value > 0 else "" - ) - _log_uri = f"{base_url}dags/{ti.dag_id}/runs/{run_id}/tasks/{ti.task_id}{map_index}{try_number}" - return _log_uri - - def parse(what: StartupDetails, log: Logger) -> RuntimeTaskInstance: # TODO: Task-SDK: # Using DagBag here is about 98% wrong, but it'll do for now diff --git a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py index 65c236a051a..525d137a481 100644 --- a/task-sdk/tests/task_sdk/execution_time/test_task_runner.py +++ b/task-sdk/tests/task_sdk/execution_time/test_task_runner.py @@ -2691,6 +2691,31 @@ class TestTaskRunnerCallsCallbacks: assert state == expected_state assert collected_results == expected_results + @pytest.mark.parametrize( + ["base_url", "expected_url"], + [ + ("http://localhost:8080/", "http://localhost:8080/dags/test_dag/runs/test_run/tasks/test_task"), + ("http://localhost:8080", "http://localhost:8080/dags/test_dag/runs/test_run/tasks/test_task"), + ( + "https://airflow.example.com/", + "https://airflow.example.com/dags/test_dag/runs/test_run/tasks/test_task", + ), + ( + "https://airflow.example.com", + "https://airflow.example.com/dags/test_dag/runs/test_run/tasks/test_task", + ), + ], + ids=["localhost_with_slash", "localhost_no_slash", "domain_with_slash", "domain_no_slash"], + ) + def test_runtime_task_instance_log_url_property(self, create_runtime_ti, base_url, expected_url): + """Test that RuntimeTaskInstance.log_url property correctly handles various base_url formats.""" + task = BaseOperator(task_id="test_task") + runtime_ti = create_runtime_ti(task=task, dag_id="test_dag", run_id="test_run", try_number=0) + + with patch("airflow.configuration.conf.get", return_value=base_url): + log_url = runtime_ti.log_url + assert log_url == expected_url + def test_task_runner_on_failure_callback_context(self, create_runtime_ti): """Test that on_failure_callback context has end_date and duration.""" from airflow.exceptions import AirflowException
