This is an automated email from the ASF dual-hosted git repository.
vincbeck 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 24faa71269a fix(providers/fab): lazily initialize flask_app in FastAPI
routes (#64908)
24faa71269a is described below
commit 24faa71269adb7799fbc082841d4e027e2645fc3
Author: Pradeep Kalluri <[email protected]>
AuthorDate: Tue Apr 14 12:42:59 2026 +0100
fix(providers/fab): lazily initialize flask_app in FastAPI routes (#64908)
* fix(providers/fab): lazily initialize flask_app in FastAPI routes
This addresses feedback on PR #64418. In certain execution flows (like
specific CLI commands or isolated tests), the FabAuthManager might be
initialized without get_fastapi_app() being called, leaving flask_app as None.
This change ensures that if flask_app is accessed and found to be None, it
is lazily initialized on demand, preventing HTTP 500 errors.
Closes #64151 follow-up.
---
.../fab/auth_manager/api_fastapi/routes/login.py | 19 ++++--
.../providers/fab/auth_manager/fab_auth_manager.py | 2 +
.../api_fastapi/routes/test_login_lazy_init.py | 70 ++++++++++++++++++++++
3 files changed, 87 insertions(+), 4 deletions(-)
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
index a59dfa0372f..7d5b9750b7b 100644
---
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
+++
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
@@ -16,6 +16,7 @@
# under the License.
from __future__ import annotations
+import logging
from typing import Any
from fastapi import Body, HTTPException, Request, status
@@ -35,6 +36,8 @@ if AIRFLOW_V_3_1_8_PLUS:
else:
get_cookie_path = lambda: "/"
+log = logging.getLogger(__name__)
+
def _get_flask_app():
from airflow.providers.fab.auth_manager.fab_auth_manager import
FabAuthManager
@@ -49,10 +52,18 @@ def _get_flask_app():
),
)
if not auth_manager.flask_app:
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Flask app is not initialized. Check that FabAuthManager
started up correctly.",
- )
+ with auth_manager._flask_app_lock:
+ if not auth_manager.flask_app:
+ try:
+ from airflow.providers.fab.www.app import create_app
+
+ auth_manager.flask_app = create_app(enable_plugins=False)
+ except Exception:
+ log.exception("Failed to lazily initialize Flask app
context for FabAuthManager")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Failed to initialize Flask app context. Check
logs for details.",
+ )
return auth_manager.flask_app
diff --git
a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
index fa8fe2a9edf..61bbf142dc2 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
@@ -18,6 +18,7 @@
from __future__ import annotations
import logging
+import threading
import warnings
from contextlib import suppress
from functools import cached_property
@@ -177,6 +178,7 @@ class FabAuthManager(BaseAuthManager[User]):
cache: TTLCache = TTLCache(maxsize=1024, ttl=CACHE_TTL)
appbuilder: AirflowAppBuilder | None = None
flask_app: Flask | None = None
+ _flask_app_lock = threading.Lock()
def init_flask_resources(self) -> None:
self._sync_appbuilder_roles()
diff --git
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login_lazy_init.py
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login_lazy_init.py
new file mode 100644
index 00000000000..d5770f4741d
--- /dev/null
+++
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login_lazy_init.py
@@ -0,0 +1,70 @@
+# 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 unittest.mock import MagicMock, patch
+
+import pytest
+from fastapi import HTTPException
+
+from airflow.providers.fab.auth_manager.api_fastapi.routes.login import
_get_flask_app
+from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
+
+
+class TestGetFlaskAppLazyInit:
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login.get_auth_manager")
+ @patch("airflow.providers.fab.www.app.create_app")
+ def test_get_flask_app_lazy_init(self, mock_create_app,
mock_get_auth_manager):
+ """Test that _get_flask_app lazily initializes the flask app if it's
None."""
+ mock_fab_auth_manager = MagicMock(spec=FabAuthManager)
+ mock_fab_auth_manager.flask_app = None
+ mock_get_auth_manager.return_value = mock_fab_auth_manager
+
+ mock_flask_app = MagicMock()
+ mock_create_app.return_value = mock_flask_app
+
+ # First call
+ result = _get_flask_app()
+
+ assert result == mock_flask_app
+ mock_create_app.assert_called_once_with(enable_plugins=False)
+ assert mock_fab_auth_manager.flask_app == mock_flask_app
+
+ # Reset mock and set flask_app (simulate it being stored)
+ mock_create_app.reset_mock()
+
+ # Second call
+ result = _get_flask_app()
+ assert result == mock_flask_app
+ mock_create_app.assert_not_called()
+
+
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login.get_auth_manager")
+ @patch("airflow.providers.fab.www.app.create_app")
+ def test_get_flask_app_lazy_init_error(self, mock_create_app,
mock_get_auth_manager):
+ """Test that _get_flask_app raises HTTPException if create_app
fails."""
+ mock_fab_auth_manager = MagicMock(spec=FabAuthManager)
+ mock_fab_auth_manager.flask_app = None
+ mock_get_auth_manager.return_value = mock_fab_auth_manager
+
+ mock_create_app.side_effect = Exception("Config error")
+
+ with pytest.raises(HTTPException) as exc:
+ _get_flask_app()
+
+ assert exc.value.status_code == 500
+ assert "Failed to initialize Flask app context" in exc.value.detail