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
« 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
19import logging
20import os
21import warnings
22from pathlib import Path
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
31from airflow.api_fastapi.auth.tokens import get_signing_key
32from airflow.exceptions import AirflowConfigException, AirflowException
34log = logging.getLogger(__name__)
36_AIRFLOW_PATH = Path(__file__).parents[3]
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
44 app.include_router(ui_router)
45 app.include_router(public_router)
47 dev_mode = os.environ.get("DEV_MODE", str(False)) == "true"
49 directory = _AIRFLOW_PATH / ("airflow/ui/dev" if dev_mode else "airflow/ui/dist")
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)
55 templates = Jinja2Templates(directory=directory)
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 )
64 app.mount(
65 "/static",
66 StaticFiles(
67 directory=directory,
68 html=True,
69 ),
70 name="webapp_static_folder",
71 )
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 )
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 )
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"})
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 )
109def init_flask_plugins(app: FastAPI) -> None:
110 """Integrate Flask plugins (plugins from Airflow 2)."""
111 from airflow import plugins_manager
113 blueprints, appbuilder_views, appbuilder_menu_links = plugins_manager.get_flask_plugins()
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
119 from fastapi.middleware.wsgi import WSGIMiddleware
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 )
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 )
136 flask_app = create_app(enable_plugins=True)
137 app.mount("/pluginsv2", WSGIMiddleware(flask_app))
140def init_config(app: FastAPI) -> None:
141 from airflow.configuration import conf
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")
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 )
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 )
170 app.state.secret_key = get_signing_key("api", "secret_key")
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
177 app.add_middleware(JWTRefreshMiddleware)
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)
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)