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

vatsrahul1001 pushed a commit to branch backport-b70a439-v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 999fa53c4577d75d9693f3c7d655b105fd312e07
Author: Rahul Vats <[email protected]>
AuthorDate: Mon Oct 26 22:36:04 2026 -0600

    [v3-4-test] Allow FastAPI 0.137+ by requiring cadwyn 8.1 (#75689)
    
    Manual backport: pruned subset of #75689 excluding the #64523 (REST 
metrics) and #74370 (dag-processor scoped Execution API) pieces that are not on 
v3-4-test. Keeps the fastapi>=0.142/cadwyn>=8.1 bump + native-OTel-disable + 
lazy-router adaptations.
    
    (cherry picked from commit b70a439a23827a140ed75498f39c429089f723b5)
---
 airflow-core/pyproject.toml                        |  9 ++---
 airflow-core/src/airflow/api_fastapi/app.py        |  2 ++
 .../src/airflow/api_fastapi/common/telemetry.py    | 38 ++++++++++++++++++++
 .../src/airflow/api_fastapi/execution_api/app.py   | 41 +++++++++++++---------
 .../execution_api/datamodels/taskinstance.py       |  3 +-
 .../airflow/api_fastapi/execution_api/security.py  | 10 +++---
 .../src/airflow/utils/serve_logs/log_server.py     |  3 +-
 .../core_api/routes/public/test_assets.py          |  5 ++-
 .../api_fastapi/core_api/routes/test_routes.py     |  8 +++--
 .../unit/api_fastapi/execution_api/test_app.py     | 29 +++++++++------
 .../execution_api/test_token_scope_boundaries.py   | 27 +++++++++-----
 .../unit/api_fastapi/logging/test_decorators.py    | 18 ++++++----
 airflow-core/tests/unit/api_fastapi/test_app.py    | 25 +++++++++++++
 .../src/tests_common/test_utils/api_fastapi.py     | 23 ++++++++++++
 .../edge3/tests/unit/edge3/worker_api/test_app.py  |  5 +--
 scripts/in_container/in_container_utils.py         | 18 ++++++----
 task-sdk/pyproject.toml                            |  2 +-
 uv.lock                                            | 22 +++++++-----
 18 files changed, 213 insertions(+), 75 deletions(-)

diff --git a/airflow-core/pyproject.toml b/airflow-core/pyproject.toml
index 480707fd6cc..08d22de3f96 100644
--- a/airflow-core/pyproject.toml
+++ b/airflow-core/pyproject.toml
@@ -81,7 +81,7 @@ dependencies = [
     "asgiref>=2.3.0; python_version < '3.14'",
     "asgiref>=3.11.1; python_version >= '3.14'",
     "attrs>=22.1.0, !=25.2.0",
-    "cadwyn>=6.1.1",
+    "cadwyn>=8.1.0",
     "colorlog>=6.8.2",
     "cron-descriptor>=1.2.24",
     "croniter>=6.2.2",
@@ -92,11 +92,8 @@ dependencies = [
     "cryptography>=44.0.3",
     "deprecated>=1.2.13",
     "dill>=0.2.2",
-    # 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",
+    # 0.142.0 is the first release with the ``telemetry`` option used to turn 
off FastAPI's native OpenTelemetry.
+    "fastapi[standard-no-fastapi-cloud-cli]>=0.142.0",
     "uvicorn>=0.37.0",
     # 1.3.1 is the first release whose urlencoded FormParser enforces 
max_fields/max_part_size
     # and reports them as a 400. Below it, an unauthenticated form body to 
POST /auth/token is
diff --git a/airflow-core/src/airflow/api_fastapi/app.py 
b/airflow-core/src/airflow/api_fastapi/app.py
index 974fb2cee75..bf8f28b4b5d 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -31,6 +31,7 @@ from airflow import settings
 from airflow.api_fastapi.common.dagbag import create_dag_bag
 from airflow.api_fastapi.common.exceptions import init_error_handlers
 from airflow.api_fastapi.common.http_access_log import HttpAccessLogMiddleware
+from airflow.api_fastapi.common.telemetry import NATIVE_TELEMETRY_DISABLED
 from airflow.api_fastapi.core_api.app import (
     init_config,
     init_flask_plugins,
@@ -134,6 +135,7 @@ def create_app(apps: str = "all") -> FastAPI:
         "supported by these parameters; regex-capable endpoints expose a 
separate parameter.",
         lifespan=lifespan,
         root_path=API_ROOT_PATH.removesuffix("/"),
+        telemetry=NATIVE_TELEMETRY_DISABLED,
         version="2",
         docs_url="/docs" if conf.getboolean("api", "enable_swagger_ui") else 
None,
         redoc_url="/redoc" if conf.getboolean("api", "enable_swagger_ui") else 
None,
diff --git a/airflow-core/src/airflow/api_fastapi/common/telemetry.py 
b/airflow-core/src/airflow/api_fastapi/common/telemetry.py
new file mode 100644
index 00000000000..6d7bae34e70
--- /dev/null
+++ b/airflow-core/src/airflow/api_fastapi/common/telemetry.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 typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+    from collections.abc import MutableMapping
+
+    from fastapi.telemetry import TelemetryConfig
+
+
+def _exclude_every_request(scope: MutableMapping[str, Any]) -> bool:
+    return True
+
+
+# Airflow controls request tracing itself (``[execution_api] 
otel_trace_propagation``, query
+# string redaction), so FastAPI's native OpenTelemetry must stay out of the 
way. Excluding every
+# request, rather than turning the signals off, also keeps the FastAPI apps 
mounted underneath
+# (auth managers, plugins) from tracing it.
+NATIVE_TELEMETRY_DISABLED: TelemetryConfig = {
+    "exclude": _exclude_every_request,
+    "auto_configure": False,
+}
diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/app.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/app.py
index 0e2271e1282..7f1fe51cc76 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/app.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/app.py
@@ -30,7 +30,7 @@ from cadwyn import (
 )
 from fastapi import Depends, FastAPI, Request, Response
 from fastapi.responses import JSONResponse
-from fastapi.routing import APIRoute
+from fastapi.routing import APIRoute, iter_route_contexts
 from opentelemetry import context as otel_context, propagate as otel_propagate
 from starlette.middleware.base import BaseHTTPMiddleware
 
@@ -40,8 +40,9 @@ from airflow.api_fastapi.auth.tokens import (
     get_sig_validation_args,
     get_signing_args,
 )
+from airflow.api_fastapi.common.telemetry import NATIVE_TELEMETRY_DISABLED
 from airflow.api_fastapi.execution_api.in_process import InProcessExecutionAPI
-from airflow.api_fastapi.execution_api.security import _REQUEST_SCOPE_TOKEN_KEY
+from airflow.api_fastapi.execution_api.security import 
_REQUEST_SCOPE_TOKEN_KEY, require_auth
 
 if TYPE_CHECKING:
     from airflow.models.dagbag import DBDagBag
@@ -260,25 +261,30 @@ async def _extract_w3c_trace_context(
             otel_context.detach(token)
 
 
+def _is_trace_context_dep(dependency) -> bool:
+    return getattr(dependency, "dependency", None) is 
_extract_w3c_trace_context
+
+
 def _inject_trace_context_dep(routes, mode: str) -> None:
-    dep = Depends(_extract_w3c_trace_context)
-    for route in routes:
+    """
+    Add trace-context extraction to the authenticated routes in 
``only-authenticated`` mode.
+
+    ``unsafe-always`` mode is handled with an app-level dependency instead, so 
extraction runs
+    before authentication declared on any enclosing router.
+    """
+    for context in iter_route_contexts(routes):
+        route = context.original_route
         if not isinstance(route, APIRoute):
             continue
         # Idempotent: create_task_execution_api_app() runs more than once per 
process
         # (cached_app + InProcessExecutionAPI), and execution_api_router is 
shared
         # module state, so strip any prior injection first.
-        route.dependencies[:] = [
-            d for d in route.dependencies if getattr(d, "dependency", None) is 
not _extract_w3c_trace_context
-        ]
-        match mode:
-            case "unsafe-always":
-                route.dependencies.insert(0, dep)
-            case "only-authenticated":
-                from airflow.api_fastapi.execution_api.security import 
require_auth
-
-                if any(getattr(d, "dependency", None) is require_auth for d in 
route.dependencies):
-                    route.dependencies.append(dep)
+        route.dependencies[:] = [d for d in route.dependencies if not 
_is_trace_context_dep(d)]
+        if mode != "only-authenticated":
+            continue
+        # context.dependencies also holds those of the enclosing routers and 
include_router() calls.
+        if any(getattr(d, "dependency", None) is require_auth for d in 
context.dependencies):
+            route.dependencies.append(Depends(_extract_w3c_trace_context))
 
 
 def create_task_execution_api_app(
@@ -303,6 +309,7 @@ def create_task_execution_api_app(
         return route.name
 
     # See https://docs.cadwyn.dev/concepts/version_changes/ for info about API 
versions
+    mode = conf.get("execution_api", "otel_trace_propagation", 
fallback="only-authenticated")
     app = CadwynWithOpenAPICustomization(
         title="Airflow Task Execution API",
         description="The private Airflow Task Execution API.",
@@ -311,13 +318,15 @@ def create_task_execution_api_app(
         api_version_parameter_name="Airflow-API-Version",
         api_version_default_value=bundle.versions[0].value,
         versions=bundle,
+        changelog_url=None,
+        dependencies=[Depends(_extract_w3c_trace_context)] if mode == 
"unsafe-always" else None,
+        telemetry=NATIVE_TELEMETRY_DISABLED,
     )
 
     # Add correlation-id middleware for request tracing
     app.add_middleware(CorrelationIdMiddleware)
     app.add_middleware(JWTReissueMiddleware)
 
-    mode = conf.get("execution_api", "otel_trace_propagation", 
fallback="only-authenticated")
     _inject_trace_context_dep(execution_api_router.routes, mode)
 
     app.generate_and_include_versioned_routers(execution_api_router)
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
index 8a1deb203fd..45bf242fad2 100644
--- 
a/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
+++ 
b/airflow-core/src/airflow/api_fastapi/execution_api/datamodels/taskinstance.py
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Annotated, Any, Literal, 
TypeVar
 
 from pydantic import (
     AwareDatetime,
+    Discriminator,
     Field,
     JsonValue,
     Tag,
@@ -297,7 +298,7 @@ TIStateUpdate = Annotated[
     | Annotated[TIRescheduleStatePayload, Tag("up_for_reschedule")]
     | Annotated[TIAwaitingInputStatePayload, Tag("awaiting_input")]
     | Annotated[TIRetryStatePayload, Tag("up_for_retry")],
-    Field(discriminator=ti_state_discriminator),
+    Discriminator(ti_state_discriminator),
 ]
 
 
diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/security.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/security.py
index 2c121b6e4ca..f209fe2da02 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/security.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/security.py
@@ -54,11 +54,13 @@ Why ``ExecutionAPIRoute`` is needed:
     to *relax* the outer router's default execution-only restriction, but
     ``SecurityScopes`` only accumulate additively — an outer dependency
     cannot see scopes declared by inner ones. ``ExecutionAPIRoute`` solves
-    this by inspecting the **merged** dependency list at route registration
-    time (after ``include_router`` has combined all parent and child
-    dependencies) and precomputing the full ``allowed_token_types`` set.
+    this by inspecting the route's own dependency list at route registration
+    time and precomputing the full ``allowed_token_types`` set.
     ``require_auth`` then reads this precomputed set from the matched route
-    at request time, avoiding the ordering problem entirely.
+    at request time, avoiding the ordering problem entirely. FastAPI includes
+    routers lazily, so ``token:*`` scopes must be declared on the route
+    itself: scopes on an enclosing router or ``include_router`` call are not
+    seen, and the route falls back to execution-only.
 
     Any router whose routes need non-default token type policies must use
     ``route_class=ExecutionAPIRoute``. Routers that only need the default
diff --git a/airflow-core/src/airflow/utils/serve_logs/log_server.py 
b/airflow-core/src/airflow/utils/serve_logs/log_server.py
index 92178bf5a24..3726d53a609 100644
--- a/airflow-core/src/airflow/utils/serve_logs/log_server.py
+++ b/airflow-core/src/airflow/utils/serve_logs/log_server.py
@@ -35,6 +35,7 @@ from jwt.exceptions import (
 
 from airflow._shared.module_loading import import_string
 from airflow.api_fastapi.auth.tokens import JWTValidator, get_signing_key
+from airflow.api_fastapi.common.telemetry import NATIVE_TELEMETRY_DISABLED
 from airflow.configuration import conf
 from airflow.utils.docs import get_docs_url
 
@@ -140,7 +141,7 @@ def create_app():
         except Exception as e:
             raise ImportError(f"Unable to load {log_config_class} due to 
error: {e}")
 
-    fastapi_app = FastAPI()
+    fastapi_app = FastAPI(telemetry=NATIVE_TELEMETRY_DISABLED)
     fastapi_app.state.signer = JWTValidator(
         issuer=None,
         secret_key=get_signing_key("api", "secret_key"),
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
index ff1e5e1d038..6ab18c19990 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_assets.py
@@ -59,6 +59,7 @@ from airflow.utils.session import provide_session
 from airflow.utils.state import DagRunState, DagSchedulingState
 from airflow.utils.types import DagRunType
 
+from tests_common.test_utils.api_fastapi import iter_api_routes
 from tests_common.test_utils.asserts import assert_queries_count
 from tests_common.test_utils.config import conf_vars
 from tests_common.test_utils.db import (
@@ -2062,7 +2063,9 @@ class TestQueuedEventsDagAxisAuthorization:
     @pytest.fixture
     def routes_by_path(self, test_client):
         return {
-            (r.path, tuple(sorted(r.methods))): r for r in 
test_client.app.routes if hasattr(r, "dependant")
+            (r.path, tuple(sorted(r.methods))): r
+            for r in iter_api_routes(test_client.app.routes)
+            if hasattr(r, "dependant")
         }
 
     @pytest.mark.parametrize(
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
index 7c2e58403ec..89d4ed29bae 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/test_routes.py
@@ -21,6 +21,8 @@ from fastapi.openapi.utils import get_openapi
 
 from airflow.api_fastapi.core_api.routes.public import authenticated_router, 
public_router
 
+from tests_common.test_utils.api_fastapi import iter_api_routes
+
 # Set of paths that are allowed to be accessible without authentication
 NO_AUTH_PATHS = {
     "/api/v2/auth/login",
@@ -35,16 +37,16 @@ def test_no_auth_routes():
     Verify that only the routes with NO_AUTH_PATHS are excluded from the 
`authenticated_router`. This
     test ensures that a router is not added to the non-authenticated router by 
mistake.
     """
-    paths_in_public_router = {route.path for route in public_router.routes}
+    paths_in_public_router = {route.path for route in 
iter_api_routes(public_router.routes)}
     paths_in_authenticated_router = {
-        f"{public_router.prefix}{route.path}" for route in 
authenticated_router.routes
+        f"{public_router.prefix}{route.path}" for route in 
iter_api_routes(authenticated_router.routes)
     }
     assert paths_in_public_router - paths_in_authenticated_router == 
NO_AUTH_PATHS
 
 
 def test_routes_with_responses():
     """Verify that each route in `public_router` has appropriate responses 
configured."""
-    for route in public_router.routes:
+    for route in iter_api_routes(public_router.routes):
         if route.path in NO_AUTH_PATHS:
             # Routes in NO_AUTH_PATHS should not have 401 or 403 response codes
             assert 401 not in route.responses, f"Route {route.path} should not 
require auth (401)"
diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py 
b/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py
index 88d210d87f0..7049fc2a60f 100644
--- a/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/execution_api/test_app.py
@@ -25,7 +25,7 @@ from uuid import UUID
 
 import httpx2
 import pytest
-from fastapi import FastAPI, Request, status
+from fastapi import APIRouter, Depends, FastAPI, Request, status
 from fastapi.params import Security as SecurityParam
 from fastapi.routing import APIRoute
 from fastapi.testclient import TestClient
@@ -38,6 +38,8 @@ from airflow.api_fastapi.app import create_app
 from airflow.api_fastapi.execution_api.app import (
     InProcessExecutionAPI,
     _extract_w3c_trace_context,
+    _inject_trace_context_dep,
+    _is_trace_context_dep,
     _jwt_generator,
     create_task_execution_api_app,
 )
@@ -48,6 +50,7 @@ from airflow.api_fastapi.execution_api.versions import bundle
 from airflow.models.xcom import XComModel
 from airflow.utils.session import create_session_async
 
+from tests_common.test_utils.api_fastapi import iter_api_routes
 from tests_common.test_utils.config import conf_vars
 
 pytestmark = pytest.mark.db_test
@@ -397,18 +400,14 @@ class TestTraceContextPropagation:
     """Exercise ``execution_api.otel_trace_propagation`` on the real Execution 
API app."""
 
     @pytest.fixture(autouse=True)
-    def _restore_router_dependencies(self):
+    def _restore_route_dependencies(self):
         from airflow.api_fastapi.execution_api.routes import 
execution_api_router
 
-        snapshot = {
-            id(route): list(route.dependencies)
-            for route in execution_api_router.routes
-            if isinstance(route, APIRoute)
-        }
+        routes = [context.original_route for context in 
iter_api_routes(execution_api_router.routes)]
+        snapshot = [list(route.dependencies) for route in routes]
         yield
-        for route in execution_api_router.routes:
-            if isinstance(route, APIRoute):
-                route.dependencies[:] = snapshot[id(route)]
+        for route, dependencies in zip(routes, snapshot):
+            route.dependencies[:] = dependencies
 
     @staticmethod
     def _build_app(mode: str):
@@ -454,6 +453,16 @@ class TestTraceContextPropagation:
         if expect_status is not None:
             assert response.status_code == expect_status
 
+    def test_only_authenticated_detects_auth_added_by_include_router(self):
+        router = APIRouter()
+        router.add_api_route("/variables/{key}", lambda key: None)
+        parent = APIRouter()
+        parent.include_router(router, dependencies=[Depends(require_auth)])
+
+        _inject_trace_context_dep(parent.routes, "only-authenticated")
+
+        assert any(_is_trace_context_dep(d) for d in 
router.routes[0].dependencies)
+
     def test_trace_context_dep_cleans_up_on_route_exception(self):
         """Verify extract and cleanup run correctly when a route handler 
raises."""
         app = self._build_app("unsafe-always")
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/test_token_scope_boundaries.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/test_token_scope_boundaries.py
index 195503f2bf0..308de1176d8 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/test_token_scope_boundaries.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/test_token_scope_boundaries.py
@@ -41,6 +41,8 @@ from fastapi.routing import APIRoute
 
 from airflow.api_fastapi.execution_api.routes import execution_api_router
 
+from tests_common.test_utils.api_fastapi import iter_api_routes
+
 # Routes that intentionally deviate from the default (execution-only) policy.
 # Any route NOT listed here must accept only {"execution"}.
 NON_DEFAULT_TOKEN_POLICY: dict[str, set[str]] = {
@@ -54,16 +56,25 @@ NON_DEFAULT_TOKEN_POLICY: dict[str, set[str]] = {
 }
 
 
+def _get_original_route(route) -> APIRoute:
+    """Return the ``APIRoute`` itself; custom attributes are not forwarded by 
FastAPI's route contexts."""
+    return getattr(route, "original_route", route)
+
+
+def _get_api_routes() -> dict[str, APIRoute]:
+    return {
+        f"{method} {route.path}": route
+        for route in iter_api_routes(execution_api_router.routes)
+        for method in route.methods or ()
+    }
+
+
 def _all_route_policies() -> dict[str, set[str]]:
     """Return a map of all API routes and their allowed token types."""
-    policy_map: dict[str, set[str]] = {}
-    for route in execution_api_router.routes:
-        if isinstance(route, APIRoute):
-            allowed_tokens = set(getattr(route, "allowed_token_types", 
{"execution"}))
-            if route.methods:
-                for method in route.methods:
-                    policy_map[f"{method} {route.path}"] = allowed_tokens
-    return policy_map
+    return {
+        key: set(getattr(_get_original_route(route), "allowed_token_types", 
{"execution"}))
+        for key, route in _get_api_routes().items()
+    }
 
 
 class TestTokenScopeBoundaries:
diff --git a/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py 
b/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py
index 66fb2fe3773..d3a25732410 100644
--- a/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py
+++ b/airflow-core/tests/unit/api_fastapi/logging/test_decorators.py
@@ -23,8 +23,7 @@ from unittest.mock import MagicMock
 
 import pytest
 from fastapi import Request
-from fastapi.dependencies.utils import get_flat_dependant
-from fastapi.routing import APIRoute
+from fastapi.dependencies.models import Dependant
 from sqlalchemy import delete, func, select
 from sqlalchemy.orm import Session
 
@@ -42,6 +41,7 @@ from airflow.models.dagbundle import DagBundleModel
 from airflow.models.team import Team
 from airflow.providers.standard.operators.empty import EmptyOperator
 
+from tests_common.test_utils.api_fastapi import iter_api_routes
 from tests_common.test_utils.config import conf_vars
 from tests_common.test_utils.db import (
     clear_db_connections,
@@ -561,27 +561,31 @@ class TestActionLoggingUnparsableBody:
         assert logged.event == "test_event"
 
 
+def _has_body_params(dependant: Dependant) -> bool:
+    """Return True if the dependant or any of its sub-dependencies declares a 
body parameter."""
+    return bool(dependant.body_params) or any(_has_body_params(dep) for dep in 
dependant.dependencies)
+
+
 def _bodyless_action_logging_routes(app) -> list[tuple[str, str]]:
     """Every ``action_logging`` route that declares no request body of its 
own."""
     found = set()
 
     def _collect(route):
-        if not isinstance(route, APIRoute):
-            return
         uses_logging = any(
             getattr(dep.call, "__qualname__", "").startswith("action_logging")
             for dep in route.dependant.dependencies
         )
         if not uses_logging:
             return
-        if get_flat_dependant(route.dependant, skip_repeats=True).body_params:
+        if _has_body_params(route.dependant):
             return
         for method in route.methods or []:
             found.add((method, route.path))
 
-    for route in app.routes:
+    for route in iter_api_routes(app.routes):
         _collect(route)
-        for sub in getattr(getattr(route, "app", None), "routes", []) or []:
+    for mount in app.routes:
+        for sub in iter_api_routes(getattr(getattr(mount, "app", None), 
"routes", []) or []):
             _collect(sub)
     return sorted(found)
 
diff --git a/airflow-core/tests/unit/api_fastapi/test_app.py 
b/airflow-core/tests/unit/api_fastapi/test_app.py
index 59ccb87007c..82e0674bafe 100644
--- a/airflow-core/tests/unit/api_fastapi/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/test_app.py
@@ -24,6 +24,9 @@ from unittest import mock
 import pytest
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+from opentelemetry.sdk.trace.export.in_memory_span_exporter import 
InMemorySpanExporter
 from sqlalchemy import event, text
 from sqlalchemy.engine import Engine
 
@@ -190,6 +193,28 @@ def 
test_access_log_middleware_installed_outermost_for_every_apps_selection(apps
     assert installed[0] is HttpAccessLogMiddleware
 
 
[email protected](
+    "path",
+    [
+        pytest.param("/api/v2/monitor/health", id="root-app"),
+        pytest.param("/auth/token", id="mounted-auth-manager-app"),
+        pytest.param("/execution/health", id="mounted-execution-app"),
+    ],
+)
+def test_fastapi_native_telemetry_records_no_spans(path):
+    exporter = InMemorySpanExporter()
+    provider = TracerProvider()
+    provider.add_span_processor(SimpleSpanProcessor(exporter))
+    traceparent = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
+
+    with mock.patch("opentelemetry.trace.get_tracer_provider", 
return_value=provider):
+        TestClient(app_module.create_app(apps="all")).get(path, 
headers={"traceparent": traceparent})
+
+    assert [
+        span.name for span in exporter.get_finished_spans() if 
span.instrumentation_scope.name == "fastapi"
+    ] == []
+
+
 def test_jwt_refresh_middleware_skips_static_mount():
     """Static UI assets are served without resolving the user from the JWT 
cookie."""
     [middleware] = [
diff --git a/devel-common/src/tests_common/test_utils/api_fastapi.py 
b/devel-common/src/tests_common/test_utils/api_fastapi.py
index 0e19c148614..06ef6a2381b 100644
--- a/devel-common/src/tests_common/test_utils/api_fastapi.py
+++ b/devel-common/src/tests_common/test_utils/api_fastapi.py
@@ -17,6 +17,8 @@
 from __future__ import annotations
 
 import json
+from collections.abc import Iterable, Iterator
+from typing import Any
 from uuid import UUID
 
 from airflow.models import DagRun, Log
@@ -102,3 +104,24 @@ def _check_task_instance_note(session, ti_id, note_data):
         session.refresh(ti_note)
         assert ti_note.content == note_data["content"]
         assert ti_note.user_id == note_data["user_id"]
+
+
+def iter_api_routes(routes: Iterable[Any]) -> Iterator[Any]:
+    """
+    Yield every ``APIRoute`` of a router or app with its full path, walking 
included routers.
+
+    FastAPI 0.137+ includes routers lazily, so this yields ``RouteContext`` 
objects; set custom
+    attributes on ``context.original_route``. Older FastAPI, still used by 
provider compatibility
+    tests, flattens included routers into ``routes``, so the ``APIRoute`` 
objects are yielded as is.
+    """
+    from fastapi.routing import APIRoute
+
+    try:
+        from fastapi.routing import iter_route_contexts
+    except ImportError:
+        yield from (route for route in routes if isinstance(route, APIRoute))
+        return
+
+    for context in iter_route_contexts(list(routes)):
+        if isinstance(context.original_route, APIRoute):
+            yield context
diff --git a/providers/edge3/tests/unit/edge3/worker_api/test_app.py 
b/providers/edge3/tests/unit/edge3/worker_api/test_app.py
index ec1c54522c9..b06567e6c40 100644
--- a/providers/edge3/tests/unit/edge3/worker_api/test_app.py
+++ b/providers/edge3/tests/unit/edge3/worker_api/test_app.py
@@ -20,11 +20,12 @@ import mimetypes
 from unittest import mock
 
 import pytest
-from fastapi.routing import APIRoute
 from starlette.routing import Mount
 
 from airflow.providers.edge3.worker_api.app import create_edge_worker_api_app
 
+from tests_common.test_utils.api_fastapi import iter_api_routes
+
 
 @pytest.fixture
 def app():
@@ -37,7 +38,7 @@ def app():
 
 
 def _api_route_paths(app) -> set[str]:
-    return {route.path for route in app.routes if isinstance(route, APIRoute)}
+    return {route.path for route in iter_api_routes(app.routes)}
 
 
 class TestCreateEdgeWorkerApiApp:
diff --git a/scripts/in_container/in_container_utils.py 
b/scripts/in_container/in_container_utils.py
index 1368f0817c0..5ee8e786c04 100644
--- a/scripts/in_container/in_container_utils.py
+++ b/scripts/in_container/in_container_utils.py
@@ -29,6 +29,8 @@ from rich.console import Console
 
 if TYPE_CHECKING:
     from fastapi import FastAPI
+    from fastapi.routing import RouteContext
+    from starlette.routing import BaseRoute
 
 
 click.rich_click.COLOR_SYSTEM = "standard"
@@ -67,13 +69,17 @@ def run_command(cmd: list[str], github_actions: bool, 
**kwargs) -> subprocess.Co
 def generate_openapi_file(app: FastAPI, file_path: Path, prefix: str = "", 
only_ui: bool = False):
     import yaml
     from fastapi.openapi.utils import get_openapi
-    from fastapi.routing import APIRoute
+    from fastapi.routing import iter_route_contexts
 
+    routes: list[BaseRoute | RouteContext] = list(app.routes)
     if only_ui:
-        for route in app.routes:
-            if not isinstance(route, APIRoute):
-                continue
-            route.include_in_schema = route.path.startswith("/ui/")
+        routes = []
+        for context in iter_route_contexts(app.routes):
+            if context.path and context.path.startswith("/ui/"):
+                # ui_router sets include_in_schema=False to keep these routes 
out of the public spec, and
+                # FastAPI fixes that flag on an included route when it first 
resolves it.
+                object.__setattr__(context, "include_in_schema", True)
+                routes.append(context)
 
     with file_path.open("w+") as f:
         openapi_schema = get_openapi(
@@ -81,7 +87,7 @@ def generate_openapi_file(app: FastAPI, file_path: Path, 
prefix: str = "", only_
             version=app.version,
             openapi_version=app.openapi_version,
             description=app.description,
-            routes=app.routes,
+            routes=routes,
         )
         if prefix:
             openapi_schema["paths"] = {
diff --git a/task-sdk/pyproject.toml b/task-sdk/pyproject.toml
index c6cafbe75bc..7b046a636f3 100644
--- a/task-sdk/pyproject.toml
+++ b/task-sdk/pyproject.toml
@@ -52,7 +52,7 @@ dependencies = [
     "asgiref>=3.11.1; python_version >= '3.14'",
     "attrs>=24.2.0, !=25.2.0",
     "babel>=2.17.0",
-    "cadwyn>=6.1.1",
+    "cadwyn>=8.1.0",
     "fsspec>=2023.10.0",
     "greenback>=1.2.1",
     "httpx2>=2.0.0",
diff --git a/uv.lock b/uv.lock
index 5a6ddbc9617..3ef22961f9b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2142,7 +2142,7 @@ requires-dist = [
     { name = "asgiref", marker = "python_full_version >= '3.14'", specifier = 
">=3.11.1" },
     { name = "attrs", specifier = ">=22.1.0,!=25.2.0" },
     { name = "cachetools", specifier = ">=6.0.0" },
-    { name = "cadwyn", specifier = ">=6.1.1" },
+    { name = "cadwyn", specifier = ">=8.1.0" },
     { name = "colorlog", specifier = ">=6.8.2" },
     { name = "cron-descriptor", specifier = ">=1.2.24" },
     { name = "croniter", specifier = ">=6.2.2" },
@@ -2150,7 +2150,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,<0.137.0" },
+    { name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier 
= ">=0.142.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" },
@@ -9458,7 +9458,7 @@ requires-dist = [
     { name = "asgiref", marker = "python_full_version >= '3.14'", specifier = 
">=3.11.1" },
     { name = "attrs", specifier = ">=24.2.0,!=25.2.0" },
     { name = "babel", specifier = ">=2.17.0" },
-    { name = "cadwyn", specifier = ">=6.1.1" },
+    { name = "cadwyn", specifier = ">=8.1.0" },
     { name = "colorlog", specifier = ">=6.8.2" },
     { name = "datadog", marker = "extra == 'all'", specifier = ">=0.50.0" },
     { name = "datadog", marker = "extra == 'datadog'", specifier = ">=0.50.0" 
},
@@ -10901,7 +10901,7 @@ wheels = [
 
 [[package]]
 name = "cadwyn"
-version = "7.0.0"
+version = "8.1.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "fastapi" },
@@ -10911,9 +10911,9 @@ dependencies = [
     { name = "typing-extensions" },
     { name = "typing-inspection" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/80/31/9a986a9fe20c6b52bde0dd23c9fc002388ab0c8f7b30a37217b07aa1875f/cadwyn-7.0.0.tar.gz";,
 hash = 
"sha256:3b57549a37e218dffb55ac5d188639de0516207f05642b084f23627c5a44d614", size 
= 662353, upload-time = "2026-06-06T16:34:39.492Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/f7/68/1f66b4f862aa678d41d7d84f16b0058b10fe65cf6d79d42693c3c9d1d64e/cadwyn-8.1.0.tar.gz";,
 hash = 
"sha256:f5fab5014c9375550844e09c0bd010108d0423972a096f0d7151d74f891ceeec", size 
= 909665, upload-time = "2026-10-22T06:42:59.275Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/73/c4/efee5781dc8b3a50fd876d413cad6db6ee69e1f21d5a07ca469cec03cd22/cadwyn-7.0.0-py3-none-any.whl";,
 hash = 
"sha256:727d3c444ae992bb2a238246d13f173e4802c92e1c901458975549e6f0522560", size 
= 61194, upload-time = "2026-06-06T16:34:37.768Z" },
+    { url = 
"https://files.pythonhosted.org/packages/23/52/c3af911a71c38e596f2f5f6c91fa9cdd92f595a0e8ded71b731a4347a675/cadwyn-8.1.0-py3-none-any.whl";,
 hash = 
"sha256:6a90765af1ce1b6ca677b358f0987ed1626495162ee0d53c82ddc32ef8d7f6fa", size 
= 120961, upload-time = "2026-10-22T06:43:00.384Z" },
 ]
 
 [[package]]
@@ -12577,18 +12577,19 @@ wheels = [
 
 [[package]]
 name = "fastapi"
-version = "0.136.3"
+version = "0.161.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "annotated-doc" },
+    { name = "opentelemetry-api" },
     { name = "pydantic" },
     { name = "starlette" },
     { name = "typing-extensions" },
     { name = "typing-inspection" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz";,
 hash = 
"sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size 
= 396410, upload-time = "2026-05-23T18:53:15.192Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/ee/12/f5a074dd4efd40898b1270551fdba19202f17591895dbc8429755128bd1b/fastapi-0.161.0.tar.gz";,
 hash = 
"sha256:c65a5226b55061349a5d5f4940a1c45da6c08ec40e58211aed9e3e1baa4e7ba5", size 
= 527298, upload-time = "2026-10-22T18:30:43.421Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl";,
 hash = 
"sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size 
= 117481, upload-time = "2026-05-23T18:53:16.924Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f3/48/3cd6f8f30857fef456c03ae1ef89c1a82aba8a3a229896c28967b49849e1/fastapi-0.161.0-py3-none-any.whl";,
 hash = 
"sha256:843d71746b18a3349e2c0b56993631788e6d89381c1973d7d8f4aa098f623ecb", size 
= 149008, upload-time = "2026-10-22T18:30:41.955Z" },
 ]
 
 [package.optional-dependencies]
@@ -12596,7 +12597,10 @@ standard-no-fastapi-cloud-cli = [
     { name = "email-validator" },
     { name = "fastapi-cli", extra = ["standard-no-fastapi-cloud-cli"] },
     { name = "httpx" },
+    { name = "httpx2" },
     { name = "jinja2" },
+    { name = "opentelemetry-exporter-otlp-proto-http" },
+    { name = "opentelemetry-sdk" },
     { name = "pydantic-extra-types" },
     { name = "pydantic-settings" },
     { name = "python-multipart" },

Reply via email to