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

shahar1 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 c9861d6750d Cap fastapi <0.137 and fix execution API health empty-path 
route (#68578)
c9861d6750d is described below

commit c9861d6750d6c45c7dc78042e71d3c1930c9710f
Author: Revanth <[email protected]>
AuthorDate: Mon Jun 15 23:50:46 2026 -0500

    Cap fastapi <0.137 and fix execution API health empty-path route (#68578)
---
 airflow-core/pyproject.toml                        |  6 +++-
 .../api_fastapi/execution_api/routes/__init__.py   |  6 +++-
 .../api_fastapi/execution_api/routes/health.py     |  4 +--
 .../api_fastapi/execution_api/routes/__init__.py}  | 31 ------------------
 .../execution_api/routes/test_health_routes.py     | 38 ++++++++++++++++++++++
 uv.lock                                            |  2 +-
 6 files changed, 51 insertions(+), 36 deletions(-)

diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml
index bcf2285eadc..31a88734c88 100644
--- a/airflow-core/pyproject.toml
+++ b/airflow-core/pyproject.toml
@@ -95,7 +95,11 @@ dependencies = [
     "cryptography>=44.0.3",
     "deprecated>=1.2.13",
     "dill>=0.2.2",
-    "fastapi[standard-no-fastapi-cloud-cli]>=0.129.0",
+    # Cap below 0.137.0: FastAPI 0.137 switched to lazy router inclusion, 
which breaks cadwyn's
+    # versioned router generation (RouterGenerationError) and fails api-server 
/ dag-processor
+    # startup. Relax once cadwyn supports FastAPI 0.137. See
+    # https://github.com/apache/airflow/issues/68562
+    "fastapi[standard-no-fastapi-cloud-cli]>=0.129.0,<0.137.0",
     "uvicorn>=0.37.0",
     "starlette>=1.0.1",
     "httpx>=0.25.0",
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py
index face45ec648..7b19f3ddd30 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/__init__.py
@@ -38,7 +38,11 @@ from airflow.api_fastapi.execution_api.routes import (
 from airflow.api_fastapi.execution_api.security import require_auth
 
 execution_api_router = APIRouter()
-execution_api_router.include_router(health.router, prefix="/health", 
tags=["Health"])
+# health.router declares its full paths ("/health", "/health/ping") and is 
included without a
+# prefix, unlike the routers below. A root route registered as @router.get("") 
under an include-time
+# prefix=... raises "Prefix and path cannot be both empty" once FastAPI 
switched to lazy router
+# inclusion (>=0.137); see https://github.com/apache/airflow/issues/68562. 
Don't reintroduce a prefix here.
+execution_api_router.include_router(health.router, tags=["Health"])
 
 # _Every_ single endpoint under here must be authenticated. Some do further 
checks on top of these
 authenticated_router = 
VersionedAPIRouter(dependencies=[Security(require_auth)])  # type: 
ignore[list-item]
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py
index d808f51e1db..9198ae33d78 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py
@@ -25,12 +25,12 @@ from airflow.api_fastapi.execution_api.deps import 
DepContainer
 router = APIRouter()
 
 
[email protected]("")
[email protected]("/health")
 def health() -> dict:
     return {"status": "healthy"}
 
 
[email protected]("/ping")
[email protected]("/health/ping")
 async def ping(services=DepContainer):
     ok: list[str] = []
     failing: dict[str, str] = {}
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py 
b/airflow-core/tests/unit/api_fastapi/execution_api/routes/__init__.py
similarity index 53%
copy from airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py
copy to airflow-core/tests/unit/api_fastapi/execution_api/routes/__init__.py
index d808f51e1db..13a83393a91 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/health.py
+++ b/airflow-core/tests/unit/api_fastapi/execution_api/routes/__init__.py
@@ -14,34 +14,3 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-
-from __future__ import annotations
-
-from fastapi import APIRouter
-from fastapi.responses import JSONResponse
-
-from airflow.api_fastapi.execution_api.deps import DepContainer
-
-router = APIRouter()
-
-
[email protected]("")
-def health() -> dict:
-    return {"status": "healthy"}
-
-
[email protected]("/ping")
-async def ping(services=DepContainer):
-    ok: list[str] = []
-    failing: dict[str, str] = {}
-    code = 200
-
-    for svc in services.get_pings():
-        try:
-            await svc.aping()
-            ok.append(svc.name)
-        except Exception as e:
-            failing[svc.name] = repr(e)
-            code = 500
-
-    return JSONResponse(content={"ok": ok, "failing": failing}, 
status_code=code)
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/routes/test_health_routes.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/routes/test_health_routes.py
new file mode 100644
index 00000000000..fc6af917205
--- /dev/null
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/routes/test_health_routes.py
@@ -0,0 +1,38 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from fastapi.routing import APIRoute
+
+
+def test_health_router_avoids_empty_root_path():
+    """Regression guard for https://github.com/apache/airflow/issues/68562 
(FastAPI >=0.137).
+
+    The break is specific: a root route registered with an empty path 
(``@router.get("")``) *and*
+    mounted under an include-time ``prefix=...`` raises ``FastAPIError: Prefix 
and path cannot be
+    both empty`` once FastAPI switched to lazy router inclusion. Older FastAPI 
merged the prefix in
+    eagerly and accepted it, so the version pin alone won't catch a 
reintroduction. The health
+    router therefore declares full, non-empty paths and is included without a 
prefix -- assert the
+    empty path stays gone, while leaving room for additional health sub-routes 
to be added later.
+    """
+    from airflow.api_fastapi.execution_api.routes import health
+
+    empty = [route.name for route in health.router.routes if isinstance(route, 
APIRoute) and not route.path]
+    assert not empty, f"Health routes must use explicit, non-empty paths 
(breaks FastAPI >=0.137): {empty}"
+
+    paths = {route.path for route in health.router.routes if isinstance(route, 
APIRoute)}
+    assert {"/health", "/health/ping"} <= paths
diff --git a/uv.lock b/uv.lock
index d506f861eb9..a164ae52924 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2067,7 +2067,7 @@ requires-dist = [
     { name = "deprecated", specifier = ">=1.2.13" },
     { name = "dill", specifier = ">=0.2.2" },
     { name = "eventlet", marker = "extra == 'async'", specifier = ">=0.37.0" },
-    { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier 
= ">=0.129.0" },
+    { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier 
= ">=0.129.0,<0.137.0" },
     { name = "gevent", marker = "extra == 'async'", specifier = ">=25.4.1" },
     { name = "graphviz", marker = "sys_platform != 'darwin' and extra == 
'graphviz'", specifier = ">=0.20" },
     { name = "greenback", marker = "extra == 'async'", specifier = ">=1.2.1" },

Reply via email to