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" },