This is an automated email from the ASF dual-hosted git repository. ash pushed a commit to branch single-base-url-for-ui in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 484ced6291f42a363739719963615592898bb53f Author: Ash Berlin-Taylor <[email protected]> AuthorDate: Mon Mar 10 14:07:33 2025 +0000 Use a single http tag to report the server's location to front end, not two Previously we had both a `<base href="">` tag and a `<meta name="backend-server-base-url" content="">` tag that contained almost the same value -- one had a trailing `/` and the other didn't. That irked me, so I updated things so the XHR requests take the URL based on the `<base>` tag instead. In order to make this change I made the following changes: - Instead of erroring if the URL does have a trailing `/`, the behaviour is now to add it if it's missing. This makes it easier kn users - This necessitated updating the Auth Managers to use a proper URL function, not string concatenation, to build the URLs --- airflow/api_fastapi/app.py | 7 ++++--- airflow/ui/dev/index.html | 2 +- airflow/ui/index.html | 3 +-- airflow/ui/src/queryClient.ts | 5 ++++- airflow/utils/helpers.py | 3 ++- .../airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py | 3 ++- .../src/airflow/providers/amazon/aws/auth_manager/router/login.py | 3 ++- .../fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py | 3 ++- providers/fab/src/airflow/providers/fab/www/views.py | 2 +- 9 files changed, 19 insertions(+), 12 deletions(-) diff --git a/airflow/api_fastapi/app.py b/airflow/api_fastapi/app.py index 3238d66bd87..707aa453756 100644 --- a/airflow/api_fastapi/app.py +++ b/airflow/api_fastapi/app.py @@ -61,9 +61,10 @@ async def lifespan(app: FastAPI): def create_app(apps: str = "all") -> FastAPI: apps_list = apps.split(",") if apps else ["all"] - fastapi_base_url = conf.get("api", "base_url") - if fastapi_base_url.endswith("/"): - raise AirflowConfigException("`[api] base_url` config option cannot have a trailing slash.") + fastapi_base_url = conf.get("api", "base_url", fallback="") + if fastapi_base_url and not fastapi_base_url.endswith("/"): + fastapi_base_url += "/" + conf.set("api", "base_url", fastapi_base_url) root_path = urlsplit(fastapi_base_url).path if not root_path or root_path == "/": diff --git a/airflow/ui/dev/index.html b/airflow/ui/dev/index.html index aab0888e331..5a99383108b 100644 --- a/airflow/ui/dev/index.html +++ b/airflow/ui/dev/index.html @@ -3,7 +3,7 @@ <html lang="en" style="height: 100%"> <head> <meta charset="UTF-8" /> - <meta name="backend-server-base-url" content="{{ backend_server_base_url }}" /> + <base href="{{ backend_server_base_url }}" /> <link rel="icon" type="image/png" href="http://localhost:5173/public/pin_32.png" /> <script type="module" src="http://localhost:5173/@vite/client"></script> <script type="module"> diff --git a/airflow/ui/index.html b/airflow/ui/index.html index 44a3b462f28..16044ec6653 100644 --- a/airflow/ui/index.html +++ b/airflow/ui/index.html @@ -2,10 +2,9 @@ <html lang="en" style="height: 100%"> <head> <meta charset="UTF-8" /> - <base href="{{ backend_server_base_url }}/" /> + <base href="{{ backend_server_base_url }}" /> <link rel="icon" type="image/png" href="/static/pin_32.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="backend-server-base-url" content="{{ backend_server_base_url }}" /> <title>Airflow 3.0</title> </head> <body style="height: 100%"> diff --git a/airflow/ui/src/queryClient.ts b/airflow/ui/src/queryClient.ts index 13452ba3782..2abfe0cc104 100644 --- a/airflow/ui/src/queryClient.ts +++ b/airflow/ui/src/queryClient.ts @@ -21,7 +21,10 @@ import { QueryClient } from "@tanstack/react-query"; import { OpenAPI } from "openapi/requests/core/OpenAPI"; // Dynamically set the base URL for XHR requests based on the meta tag. -OpenAPI.BASE = document.querySelector("meta[name='backend-server-base-url']")?.getAttribute("content") ?? ""; +OpenAPI.BASE = document.querySelector("head>base")?.getAttribute("href") ?? ""; +if (OpenAPI.BASE.endsWith("/")) { + OpenAPI.BASE = OpenAPI.BASE.slice(0, -1); +} export const queryClient = new QueryClient({ defaultOptions: { diff --git a/airflow/utils/helpers.py b/airflow/utils/helpers.py index 86730198ee4..9a7b781c2b6 100644 --- a/airflow/utils/helpers.py +++ b/airflow/utils/helpers.py @@ -25,6 +25,7 @@ from collections.abc import Generator, Iterable, Mapping, MutableMapping from datetime import datetime from functools import cache, reduce from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast +from urllib.parse import urljoin from lazy_object_proxy import Proxy @@ -259,7 +260,7 @@ def build_airflow_dagrun_url(dag_id: str, run_id: str) -> str: http://localhost:8080/dags/hi/runs/manual__2025-02-23T18:27:39.051358+00:00_RZa1at4Q """ baseurl = conf.get("api", "base_url") - return f"{baseurl}/dags/{dag_id}/runs/{run_id}" + return urljoin(baseurl, "dags/{dag_id}/runs/{run_id}") # The 'template' argument is typed as Any because the jinja2.Template is too diff --git a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py index 0c0c236e0b1..d7ec352bec3 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py @@ -21,6 +21,7 @@ from collections import defaultdict from collections.abc import Sequence from functools import cached_property from typing import TYPE_CHECKING, Any, cast +from urllib.parse import urljoin from fastapi import FastAPI @@ -321,7 +322,7 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]): return {dag_id for dag_id in dag_ids if _has_access_to_dag(requests[dag_id][method])} def get_url_login(self, **kwargs) -> str: - return f"{self.apiserver_endpoint}/auth/login" + return urljoin(self.apiserver_endpoint, "auth/login") @staticmethod def get_cli_commands() -> list[CLICommand]: diff --git a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py index a4c04465aa8..2830f353b34 100644 --- a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py +++ b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py @@ -19,6 +19,7 @@ from __future__ import annotations import logging from typing import Any +from urllib.parse import urljoin import anyio from fastapi import HTTPException, Request @@ -79,7 +80,7 @@ def login_callback(request: Request): username=saml_auth.get_nameid(), email=attributes["email"][0] if "email" in attributes else None, ) - url = f"{conf.get('api', 'base_url')}/?token={get_auth_manager().get_jwt_token(user)}" + url = urljoin(conf.get("api", "base_url"), f"?token={get_auth_manager().get_jwt_token(user)}") return RedirectResponse(url=url, status_code=303) 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 0c721e713a1..c79d1313ef7 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 @@ -21,6 +21,7 @@ import argparse from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any +from urllib.parse import urljoin import packaging.version from connexion import FlaskApi @@ -411,7 +412,7 @@ class FabAuthManager(BaseAuthManager[User]): def get_url_login(self, **kwargs) -> str: """Return the login page url.""" - return f"{self.apiserver_endpoint}/auth/login" + return urljoin(self.apiserver_endpoint, "auth/login") def get_url_logout(self): """Return the logout page url.""" diff --git a/providers/fab/src/airflow/providers/fab/www/views.py b/providers/fab/src/airflow/providers/fab/www/views.py index 3cac6d91636..22cd5def120 100644 --- a/providers/fab/src/airflow/providers/fab/www/views.py +++ b/providers/fab/src/airflow/providers/fab/www/views.py @@ -70,7 +70,7 @@ class FabIndexView(IndexView): def index(self): if g.user is not None and g.user.is_authenticated: token = get_auth_manager().get_jwt_token(g.user) - return redirect(f"{conf.get('api', 'base_url')}/?token={token}", code=302) + return redirect(urljoin(conf.get("api", "base_url"), f"?token={token}"), code=302) else: return super().index()
