This is an automated email from the ASF dual-hosted git repository.
ash 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 245d26dac17 Use a single http tag to report the server's location to
front end, not two (#47572)
245d26dac17 is described below
commit 245d26dac17b0b5a5443e26848589b6c66561eea
Author: Ash Berlin-Taylor <[email protected]>
AuthorDate: Tue Mar 11 15:34:14 2025 +0000
Use a single http tag to report the server's location to front end, not two
(#47572)
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
The change to `conf.set` is a side-effect/fix to allow `conf.set("api",
"base_url")` to work -- otherwise it errored with "No section found" with
only
the defaults present.
---
airflow/api_fastapi/app.py | 11 +++++------
airflow/api_fastapi/auth/managers/simple/routes/login.py | 4 +++-
airflow/configuration.py | 5 +++++
airflow/ui/dev/index.html | 2 +-
airflow/ui/index.html | 3 +--
airflow/ui/src/queryClient.ts | 5 ++++-
airflow/utils/helpers.py | 3 ++-
.../providers/amazon/aws/auth_manager/aws_auth_manager.py | 3 ++-
.../airflow/providers/amazon/aws/auth_manager/router/login.py | 3 ++-
.../airflow/providers/fab/auth_manager/fab_auth_manager.py | 3 ++-
providers/fab/src/airflow/providers/fab/www/views.py | 2 +-
11 files changed, 28 insertions(+), 16 deletions(-)
diff --git a/airflow/api_fastapi/app.py b/airflow/api_fastapi/app.py
index 3238d66bd87..6c996373015 100644
--- a/airflow/api_fastapi/app.py
+++ b/airflow/api_fastapi/app.py
@@ -61,13 +61,12 @@ 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 == "/":
- root_path = ""
+ root_path = urlsplit(fastapi_base_url).path.removesuffix("/")
app = FastAPI(
title="Airflow API",
diff --git a/airflow/api_fastapi/auth/managers/simple/routes/login.py
b/airflow/api_fastapi/auth/managers/simple/routes/login.py
index c8c92befeea..f3560cee16f 100644
--- a/airflow/api_fastapi/auth/managers/simple/routes/login.py
+++ b/airflow/api_fastapi/auth/managers/simple/routes/login.py
@@ -17,6 +17,8 @@
from __future__ import annotations
+from urllib.parse import urljoin
+
from fastapi import HTTPException, status
from starlette.responses import RedirectResponse
@@ -60,7 +62,7 @@ def create_token_all_admins() -> RedirectResponse:
username="Anonymous",
role="ADMIN",
)
- 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)
diff --git a/airflow/configuration.py b/airflow/configuration.py
index 2e1ad03e4d1..2d01e83c108 100644
--- a/airflow/configuration.py
+++ b/airflow/configuration.py
@@ -1289,6 +1289,11 @@ class AirflowConfigParser(ConfigParser):
"""
section = section.lower()
option = option.lower()
+ defaults = self.configuration_description or {}
+ if not self.has_section(section) and section in defaults:
+ # Trying to set a key in a section that exists in default, but not
in the user config;
+ # automatically create it
+ self.add_section(section)
super().set(section, option, value)
def remove_option(self, section: str, option: str, remove_default: bool =
True):
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..d2fc13eaa19 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, f"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 3b207a0fc9a..f7800a69075 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
@@ -409,7 +410,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()