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()
 

Reply via email to