Coverage for /home/airflow/.local/lib/python3.12/site-packages/airflow/api_fastapi/core_api/app.py: 76%

69 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 14:22 +0000

1# Licensed to the Apache Software Foundation (ASF) under one 

2# or more contributor license agreements. See the NOTICE file 

3# distributed with this work for additional information 

4# regarding copyright ownership. The ASF licenses this file 

5# to you under the Apache License, Version 2.0 (the 

6# "License"); you may not use this file except in compliance 

7# with the License. You may obtain a copy of the License at 

8# 

9# http://www.apache.org/licenses/LICENSE-2.0 

10# 

11# Unless required by applicable law or agreed to in writing, 

12# software distributed under the License is distributed on an 

13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 

14# KIND, either express or implied. See the License for the 

15# specific language governing permissions and limitations 

16# under the License. 

17from __future__ import annotations 

18 

19import logging 

20import os 

21import warnings 

22from pathlib import Path 

23 

24from fastapi import FastAPI, Request 

25from fastapi.middleware.cors import CORSMiddleware 

26from fastapi.middleware.gzip import GZipMiddleware 

27from fastapi.responses import HTMLResponse, JSONResponse 

28from fastapi.staticfiles import StaticFiles 

29from fastapi.templating import Jinja2Templates 

30 

31from airflow.api_fastapi.auth.tokens import get_signing_key 

32from airflow.exceptions import AirflowConfigException, AirflowException 

33 

34log = logging.getLogger(__name__) 

35 

36_AIRFLOW_PATH = Path(__file__).parents[3] 

37 

38 

39def init_views(app: FastAPI) -> None: 

40 """Init views by registering the different routers.""" 

41 from airflow.api_fastapi.core_api.routes.public import public_router 

42 from airflow.api_fastapi.core_api.routes.ui import ui_router 

43 

44 app.include_router(ui_router) 

45 app.include_router(public_router) 

46 

47 dev_mode = os.environ.get("DEV_MODE", str(False)) == "true" 

48 

49 directory = _AIRFLOW_PATH / ("airflow/ui/dev" if dev_mode else "airflow/ui/dist") 

50 

51 # During python tests or when the backend is run without having the frontend build 

52 # those directories might not exist. App should not fail initializing in those scenarios. 

53 Path(directory).mkdir(exist_ok=True) 

54 

55 templates = Jinja2Templates(directory=directory) 

56 

57 if dev_mode: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true

58 app.mount( 

59 "/static/i18n/locales", 

60 StaticFiles(directory=_AIRFLOW_PATH / "airflow/ui/public/i18n/locales"), 

61 name="dev_i18n_static", 

62 ) 

63 

64 app.mount( 

65 "/static", 

66 StaticFiles( 

67 directory=directory, 

68 html=True, 

69 ), 

70 name="webapp_static_folder", 

71 ) 

72 

73 @app.get("/health", include_in_schema=False) 

74 def old_health(): 

75 # If someone has the `/health` endpoint from Airflow 2 set up, we want this to be a 404, not serve the 

76 # default index.html for the SPA. 

77 # 

78 # This is a 404, not a redirect, as setups need correcting to account for this, and a redirect might 

79 # hide the issue 

80 return JSONResponse( 

81 status_code=404, 

82 content={"error": "Moved in Airflow 3. Please change config to check `/api/v2/monitor/health`"}, 

83 ) 

84 

85 @app.get("/api/v1/{_:path}", include_in_schema=False) 

86 def old_api(_): 

87 return JSONResponse( 

88 status_code=404, 

89 content={ 

90 "error": "/api/v1 has been removed in Airflow 3, please use its upgraded version /api/v2 instead." 

91 }, 

92 ) 

93 

94 @app.get("/api/{_:path}", include_in_schema=False) 

95 def api_not_found(_): 

96 """Catch all route to handle invalid API endpoints.""" 

97 return JSONResponse(status_code=404, content={"error": "API route not found"}) 

98 

99 @app.get("/{rest_of_path:path}", response_class=HTMLResponse, include_in_schema=False) 

100 def webapp(request: Request, rest_of_path: str): 

101 return templates.TemplateResponse( 

102 request, 

103 "/index.html", 

104 {"backend_server_base_url": request.base_url.path}, 

105 media_type="text/html", 

106 ) 

107 

108 

109def init_flask_plugins(app: FastAPI) -> None: 

110 """Integrate Flask plugins (plugins from Airflow 2).""" 

111 from airflow import plugins_manager 

112 

113 blueprints, appbuilder_views, appbuilder_menu_links = plugins_manager.get_flask_plugins() 

114 

115 # If no Airflow 2.x plugin is in the environment, no need to go further 

116 if not blueprints and not appbuilder_views and not appbuilder_menu_links: 116 ↛ 119line 116 didn't jump to line 119 because the condition on line 116 was always true

117 return 

118 

119 from fastapi.middleware.wsgi import WSGIMiddleware 

120 

121 try: 

122 from airflow.providers.fab.www.app import create_app 

123 except ImportError: 

124 raise AirflowException( 

125 "Some Airflow 2 plugins have been detected in your environment. " 

126 "To run them with Airflow 3, you must install the FAB provider in your Airflow environment." 

127 ) 

128 

129 warnings.warn( 

130 "You have a plugin that is using a FAB view or Flask Blueprint, which was used for the Airflow 2 UI," 

131 "and is now deprecated. Please update your plugin to be compatible with the Airflow 3 UI.", 

132 DeprecationWarning, 

133 stacklevel=2, 

134 ) 

135 

136 flask_app = create_app(enable_plugins=True) 

137 app.mount("/pluginsv2", WSGIMiddleware(flask_app)) 

138 

139 

140def init_config(app: FastAPI) -> None: 

141 from airflow.configuration import conf 

142 

143 allow_origins = conf.getlist("api", "access_control_allow_origins") 

144 allow_methods = conf.getlist("api", "access_control_allow_methods") 

145 allow_headers = conf.getlist("api", "access_control_allow_headers") 

146 

147 if "*" in allow_origins: 147 ↛ 154line 147 didn't jump to line 154 because the condition on line 147 was never true

148 # The CORS spec forbids combining `Access-Control-Allow-Origin: *` with 

149 # `Access-Control-Allow-Credentials: true`, and browsers reject any response that does so 

150 # (see https://fetch.spec.whatwg.org/#cors-protocol-and-credentials). Airflow's API needs 

151 # credentialed requests for cookie / Authorization-header auth, so a wildcard origin is 

152 # never a valid configuration. Fail loudly at startup instead of silently shipping a 

153 # response shape that no browser will accept. 

154 raise AirflowConfigException( 

155 "`[api] access_control_allow_origins` must not contain `*`: the wildcard origin is " 

156 "incompatible with the credentialed CORS Airflow's API requires, and browsers will " 

157 "reject every cross-origin response. List the exact origins that need access " 

158 "(e.g. `https://airflow.mycompany.com`) instead." 

159 ) 

160 

161 if allow_origins or allow_methods or allow_headers: 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true

162 app.add_middleware( 

163 CORSMiddleware, 

164 allow_origins=allow_origins, 

165 allow_credentials=True, 

166 allow_methods=allow_methods, 

167 allow_headers=allow_headers, 

168 ) 

169 

170 app.state.secret_key = get_signing_key("api", "secret_key") 

171 

172 

173def init_middlewares(app: FastAPI) -> None: 

174 from airflow.api_fastapi.app import get_auth_manager 

175 from airflow.api_fastapi.auth.middlewares.refresh_token import JWTRefreshMiddleware 

176 

177 app.add_middleware(JWTRefreshMiddleware) 

178 

179 for middleware_cls, middleware_kwargs in get_auth_manager().get_fastapi_middlewares(): 179 ↛ 180line 179 didn't jump to line 180 because the loop on line 179 never started

180 app.add_middleware(middleware_cls, **middleware_kwargs) 

181 

182 # GZipMiddleware must be inside HttpAccessLogMiddleware so that access logs capture 

183 # the full end-to-end duration including compression time. HttpAccessLogMiddleware is 

184 # installed by ``init_access_logging`` in ``create_app``, which runs after this 

185 # function — do not reorder those calls. 

186 # See https://github.com/apache/airflow/issues/60165 

187 app.add_middleware(GZipMiddleware, minimum_size=1024, compresslevel=5)