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

kaxil 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 0a913f21aa8 Use the configured API port in the default execution API 
URL (#74478)
0a913f21aa8 is described below

commit 0a913f21aa8138544da3b60e20e4f35092825500
Author: Kaxil Naik <[email protected]>
AuthorDate: Fri Oct 9 16:22:57 2026 +0100

    Use the configured API port in the default execution API URL (#74478)
    
    When [api] base_url is unset or relative, the execution API URL fell
    back to http://localhost:8080, ignoring [api] port. A single-host setup
    that changed [api] port without setting base_url failed every task with
    "Connection refused". The fallback now uses [api] port.
    
    Also document how tasks reach an execution API server that runs
    separately from the core API server.
---
 .../administration-and-deployment/web-stack.rst    | 13 +++++++--
 .../src/airflow/config_templates/config.yml        |  5 ++--
 .../src/airflow/executors/base_executor.py         | 14 +++++-----
 .../tests/unit/executors/test_local_executor.py    | 32 ++++++++++++++++++++++
 4 files changed, 52 insertions(+), 12 deletions(-)

diff --git a/airflow-core/docs/administration-and-deployment/web-stack.rst 
b/airflow-core/docs/administration-and-deployment/web-stack.rst
index ed8264efc4b..7a61d27c59d 100644
--- a/airflow-core/docs/administration-and-deployment/web-stack.rst
+++ b/airflow-core/docs/administration-and-deployment/web-stack.rst
@@ -31,9 +31,9 @@ now be available through that additional ``d12345`` prefix. 
Without rebuilding
 the frontend, XHR requests and static file queries should be directed to the 
prefixed url
 and served successfully.
 
-You will also need to update the execution API server url
-:ref:`config:core__execution_api_server_url` for tasks to be able to reach the 
API
-with the new prefix.
+The default execution API server url 
:ref:`config:core__execution_api_server_url` follows
+``base_url``, so tasks reach the API with the new prefix. If you set 
``execution_api_server_url``
+explicitly, add the prefix to it as well.
 
 Separating API Servers
 -----------------------
@@ -58,6 +58,13 @@ separately. This might be useful for scaling them 
independently or for deploying
    # serve only the Execution API Server
    airflow api-server --apps execution
 
+Tasks call the Execution API at :ref:`config:core__execution_api_server_url`, 
which defaults to
+``{base_url}/execution/`` (see :ref:`config:api__base_url`). When you separate 
the servers, either
+have your proxy route that path to the Execution API Server, or point 
``execution_api_server_url``
+at it wherever tasks run, for example 
``http://airflow-execution-api:8080/execution/``. If
+``base_url`` has a path such as ``/d12345``, the Execution API Server serves 
under that path too,
+so include it: ``http://airflow-execution-api:8080/d12345/execution/``.
+
 Known Issues
 ------------
 
diff --git a/airflow-core/src/airflow/config_templates/config.yml 
b/airflow-core/src/airflow/config_templates/config.yml
index 7581497d0c5..0de787a4c59 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -557,8 +557,9 @@ core:
     execution_api_server_url:
       description: |
         The url of the execution api server. Default is 
``{BASE_URL}/execution/``
-        where ``{BASE_URL}`` is the base url of the API Server. If 
``{BASE_URL}`` is not set,
-        it will use ``http://localhost:8080`` as the default base url.
+        where ``{BASE_URL}`` is the base url of the API Server. If 
``{BASE_URL}`` is not set
+        or is a relative path such as ``/airflow``, it is prefixed with 
``http://localhost:{PORT}``,
+        where ``{PORT}`` is ``[api] port``.
       version_added: 3.0.0
       type: string
       example: ~
diff --git a/airflow-core/src/airflow/executors/base_executor.py 
b/airflow-core/src/airflow/executors/base_executor.py
index b99ad38a4d4..c7898c347ed 100644
--- a/airflow-core/src/airflow/executors/base_executor.py
+++ b/airflow-core/src/airflow/executors/base_executor.py
@@ -25,7 +25,7 @@ from collections import defaultdict, deque
 from collections.abc import Sequence
 from dataclasses import dataclass, field
 from functools import cached_property, wraps
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, Any, ClassVar
 
 import pendulum
 
@@ -58,15 +58,15 @@ def get_execution_api_server_url(conf_source: 
AirflowConfigParser | ExecutorConf
         Team-specific executors can pass their own config (e.g. 
``ExecutorConf``) to resolve
         a team-specific URL.
     """
+    execution_api_server_url = conf_source.get("core", 
"execution_api_server_url", fallback=None)
+    if execution_api_server_url is not None:
+        return execution_api_server_url
     base_url = conf_source.get("api", "base_url", fallback="/")
     # ExecutorConf.get() is typed as str | None even when fallback= guarantees 
a str,
-    # so the `not base_url` guard and the cast() below keep mypy happy.
+    # so the `not base_url` guard keeps mypy happy.
     if not base_url or base_url.startswith("/"):
-        base_url = f"http://localhost:8080{base_url}";
-    default_execution_api_server = f"{base_url.rstrip('/')}/execution/"
-    return cast(
-        "str", conf_source.get("core", "execution_api_server_url", 
fallback=default_execution_api_server)
-    )
+        base_url = f"http://localhost:{conf_source.getint('api', 
'port')}{base_url}"
+    return f"{base_url.rstrip('/')}/execution/"
 
 
 if TYPE_CHECKING:
diff --git a/airflow-core/tests/unit/executors/test_local_executor.py 
b/airflow-core/tests/unit/executors/test_local_executor.py
index 2c6e1d15484..9f5d196d6cd 100644
--- a/airflow-core/tests/unit/executors/test_local_executor.py
+++ b/airflow-core/tests/unit/executors/test_local_executor.py
@@ -463,6 +463,22 @@ class TestLocalExecutor:
             ({}, "http://localhost:8080/execution/";),
             ({("api", "base_url"): "/"}, "http://localhost:8080/execution/";),
             ({("api", "base_url"): "/airflow/"}, 
"http://localhost:8080/airflow/execution/";),
+            ({("api", "port"): "9091"}, "http://localhost:9091/execution/";),
+            (
+                {("api", "base_url"): "/airflow/", ("api", "port"): "9091"},
+                "http://localhost:9091/airflow/execution/";,
+            ),
+            (
+                {("api", "base_url"): "http://test-server";, ("api", "port"): 
"9091"},
+                "http://test-server/execution/";,
+            ),
+            (
+                {
+                    ("core", "execution_api_server_url"): 
"http://custom-server/execution/";,
+                    ("api", "port"): "not-a-port",
+                },
+                "http://custom-server/execution/";,
+            ),
         ],
         ids=[
             "base_url_fallback",
@@ -470,6 +486,10 @@ class TestLocalExecutor:
             "no_base_url_no_custom",
             "base_url_no_custom",
             "relative_base_url",
+            "no_base_url_custom_port",
+            "relative_base_url_custom_port",
+            "absolute_base_url_ignores_port",
+            "custom_server_ignores_port",
         ],
     )
     @mock.patch("airflow.executors.base_executor.BaseExecutor.run_workload")
@@ -526,6 +546,18 @@ class TestLocalExecutor:
                 assert mock_run_workload.call_count == 1
                 assert mock_run_workload.call_args.kwargs["server"] == 
default_server
 
+    def test_execution_api_server_url_fallback_uses_team_api_port(self):
+        """A team executor builds the fallback URL from its own ``[api] 
port``, not the global one."""
+        with (
+            mock.patch.dict(os.environ, {"AIRFLOW__TEAM_A___API__PORT": 
"9092"}),
+            conf_vars({("api", "port"): "9091"}),
+        ):
+            team_url = 
get_execution_api_server_url(ExecutorConf(team_name="team_a"))
+            global_url = 
get_execution_api_server_url(ExecutorConf(team_name=None))
+
+        assert team_url == "http://localhost:9092/execution/";
+        assert global_url == "http://localhost:9091/execution/";
+
     def test_multiple_team_executors_isolation(self):
         """Test that multiple team executors can coexist with isolated 
resources"""
         team_a_executor = LocalExecutor(parallelism=2, team_name="team_a")

Reply via email to