Skip to content

Commit a496f10

Browse files
ashbnailo2c
authored andcommitted
Use a single http tag to report the server's location to front end, not two (apache#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.
1 parent 5c3206a commit a496f10

File tree

11 files changed

+28
-16
lines changed

11 files changed

+28
-16
lines changed

airflow/api_fastapi/app.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,12 @@ async def lifespan(app: FastAPI):
6161
def create_app(apps: str = "all") -> FastAPI:
6262
apps_list = apps.split(",") if apps else ["all"]
6363

64-
fastapi_base_url = conf.get("api", "base_url")
65-
if fastapi_base_url.endswith("/"):
66-
raise AirflowConfigException("`[api] base_url` config option cannot have a trailing slash.")
64+
fastapi_base_url = conf.get("api", "base_url", fallback="")
65+
if fastapi_base_url and not fastapi_base_url.endswith("/"):
66+
fastapi_base_url += "/"
67+
conf.set("api", "base_url", fastapi_base_url)
6768

68-
root_path = urlsplit(fastapi_base_url).path
69-
if not root_path or root_path == "/":
70-
root_path = ""
69+
root_path = urlsplit(fastapi_base_url).path.removesuffix("/")
7170

7271
app = FastAPI(
7372
title="Airflow API",

airflow/api_fastapi/auth/managers/simple/routes/login.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
from __future__ import annotations
1919

20+
from urllib.parse import urljoin
21+
2022
from fastapi import HTTPException, status
2123
from starlette.responses import RedirectResponse
2224

@@ -60,7 +62,7 @@ def create_token_all_admins() -> RedirectResponse:
6062
username="Anonymous",
6163
role="ADMIN",
6264
)
63-
url = f"{conf.get('api', 'base_url')}/?token={get_auth_manager().get_jwt_token(user)}"
65+
url = urljoin(conf.get("api", "base_url"), f"?token={get_auth_manager().get_jwt_token(user)}")
6466
return RedirectResponse(url=url)
6567

6668

airflow/configuration.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,11 @@ def set(self, section: str, option: str, value: str | None = None) -> None:
12891289
"""
12901290
section = section.lower()
12911291
option = option.lower()
1292+
defaults = self.configuration_description or {}
1293+
if not self.has_section(section) and section in defaults:
1294+
# Trying to set a key in a section that exists in default, but not in the user config;
1295+
# automatically create it
1296+
self.add_section(section)
12921297
super().set(section, option, value)
12931298

12941299
def remove_option(self, section: str, option: str, remove_default: bool = True):

airflow/ui/dev/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<html lang="en" style="height: 100%">
44
<head>
55
<meta charset="UTF-8" />
6-
<meta name="backend-server-base-url" content="{{ backend_server_base_url }}" />
6+
<base href="{{ backend_server_base_url }}" />
77
<link rel="icon" type="image/png" href="http://localhost:5173/public/pin_32.png" />
88
<script type="module" src="http://localhost:5173/@vite/client"></script>
99
<script type="module">

airflow/ui/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
<html lang="en" style="height: 100%">
33
<head>
44
<meta charset="UTF-8" />
5-
<base href="{{ backend_server_base_url }}/" />
5+
<base href="{{ backend_server_base_url }}" />
66
<link rel="icon" type="image/png" href="/static/pin_32.png" />
77
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8-
<meta name="backend-server-base-url" content="{{ backend_server_base_url }}" />
98
<title>Airflow 3.0</title>
109
</head>
1110
<body style="height: 100%">

airflow/ui/src/queryClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import { QueryClient } from "@tanstack/react-query";
2121
import { OpenAPI } from "openapi/requests/core/OpenAPI";
2222

2323
// Dynamically set the base URL for XHR requests based on the meta tag.
24-
OpenAPI.BASE = document.querySelector("meta[name='backend-server-base-url']")?.getAttribute("content") ?? "";
24+
OpenAPI.BASE = document.querySelector("head>base")?.getAttribute("href") ?? "";
25+
if (OpenAPI.BASE.endsWith("/")) {
26+
OpenAPI.BASE = OpenAPI.BASE.slice(0, -1);
27+
}
2528

2629
export const queryClient = new QueryClient({
2730
defaultOptions: {

airflow/utils/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from datetime import datetime
2626
from functools import cache, reduce
2727
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
28+
from urllib.parse import urljoin
2829

2930
from lazy_object_proxy import Proxy
3031

@@ -259,7 +260,7 @@ def build_airflow_dagrun_url(dag_id: str, run_id: str) -> str:
259260
http://localhost:8080/dags/hi/runs/manual__2025-02-23T18:27:39.051358+00:00_RZa1at4Q
260261
"""
261262
baseurl = conf.get("api", "base_url")
262-
return f"{baseurl}/dags/{dag_id}/runs/{run_id}"
263+
return urljoin(baseurl, f"dags/{dag_id}/runs/{run_id}")
263264

264265

265266
# The 'template' argument is typed as Any because the jinja2.Template is too

providers/amazon/src/airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from collections.abc import Sequence
2222
from functools import cached_property
2323
from typing import TYPE_CHECKING, Any, cast
24+
from urllib.parse import urljoin
2425

2526
from fastapi import FastAPI
2627

@@ -321,7 +322,7 @@ def _has_access_to_dag(request: IsAuthorizedRequest):
321322
return {dag_id for dag_id in dag_ids if _has_access_to_dag(requests[dag_id][method])}
322323

323324
def get_url_login(self, **kwargs) -> str:
324-
return f"{self.apiserver_endpoint}/auth/login"
325+
return urljoin(self.apiserver_endpoint, "auth/login")
325326

326327
@staticmethod
327328
def get_cli_commands() -> list[CLICommand]:

providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import logging
2121
from typing import Any
22+
from urllib.parse import urljoin
2223

2324
import anyio
2425
from fastapi import HTTPException, Request
@@ -79,7 +80,7 @@ def login_callback(request: Request):
7980
username=saml_auth.get_nameid(),
8081
email=attributes["email"][0] if "email" in attributes else None,
8182
)
82-
url = f"{conf.get('api', 'base_url')}/?token={get_auth_manager().get_jwt_token(user)}"
83+
url = urljoin(conf.get("api", "base_url"), f"?token={get_auth_manager().get_jwt_token(user)}")
8384
return RedirectResponse(url=url, status_code=303)
8485

8586

providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from functools import cached_property
2222
from pathlib import Path
2323
from typing import TYPE_CHECKING, Any
24+
from urllib.parse import urljoin
2425

2526
import packaging.version
2627
from connexion import FlaskApi
@@ -409,7 +410,7 @@ def security_manager(self) -> FabAirflowSecurityManagerOverride:
409410

410411
def get_url_login(self, **kwargs) -> str:
411412
"""Return the login page url."""
412-
return f"{self.apiserver_endpoint}/auth/login/"
413+
return urljoin(self.apiserver_endpoint, "auth/login/")
413414

414415
def get_url_logout(self):
415416
"""Return the logout page url."""

0 commit comments

Comments
 (0)