This is an automated email from the ASF dual-hosted git repository. vatsrahul1001 pushed a commit to branch backport-322-66690 in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 0764c18575a2357e3d3f18db5847eaaaeec92086 Author: Sai Teja Desu <[email protected]> AuthorDate: Wed May 13 07:40:04 2026 -0700 UI: Preserve proxied URL on login redirect (#66690) When the UI is reached through a proxy (Gitpod, Codespaces, ngrok, reverse proxies), the auth-failure interceptor sent the API server's absolute URL as the 'next' parameter, so post-login redirects went to e.g. http://localhost:29091 instead of the URL the browser is on. Send a same-origin path+search+hash instead, so the browser stays on whatever origin it is currently using. Add regression coverage for proxied subpaths so redirects also preserve base paths such as /team-a/. closes: #46533 (cherry picked from commit 25ef835185cb268a800ce117ecb88c6eea81ce77) --- airflow-core/src/airflow/ui/src/main.tsx | 4 +- .../src/airflow/ui/src/utils/links.test.ts | 52 +++++++++++++++++++++- airflow-core/src/airflow/ui/src/utils/links.ts | 7 +++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx index 9fb55690e4c..4daeb12b19e 100644 --- a/airflow-core/src/airflow/ui/src/main.tsx +++ b/airflow-core/src/airflow/ui/src/main.tsx @@ -34,7 +34,7 @@ import { ChakraCustomProvider } from "src/context/ChakraCustomProvider"; import { ColorModeProvider } from "src/context/colorMode"; import { TimezoneProvider } from "src/context/timezone"; import { router } from "src/router"; -import { getRedirectPath } from "src/utils/links.ts"; +import { getNextHref, getRedirectPath } from "src/utils/links.ts"; import i18n from "./i18n/config"; import { client } from "./queryClient"; @@ -75,7 +75,7 @@ axios.interceptors.response.use( ) { const params = new URLSearchParams(); - params.set("next", globalThis.location.href); + params.set("next", getNextHref(globalThis.location)); const loginPath = getRedirectPath("api/v2/auth/login"); globalThis.location.replace(`${loginPath}?${params.toString()}`); diff --git a/airflow-core/src/airflow/ui/src/utils/links.test.ts b/airflow-core/src/airflow/ui/src/utils/links.test.ts index 542f508a962..2dc176258c6 100644 --- a/airflow-core/src/airflow/ui/src/utils/links.test.ts +++ b/airflow-core/src/airflow/ui/src/utils/links.test.ts @@ -1,3 +1,5 @@ +/* eslint-disable max-lines */ + /*! * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -20,7 +22,12 @@ import { describe, it, expect } from "vitest"; import type { TaskInstanceResponse } from "openapi/requests/types.gen"; -import { buildTaskInstanceUrl, getTaskInstanceAdditionalPath, getTaskInstanceLink } from "./links"; +import { + buildTaskInstanceUrl, + getNextHref, + getTaskInstanceAdditionalPath, + getTaskInstanceLink, +} from "./links"; describe("getTaskInstanceLink", () => { const testCases = [ @@ -284,3 +291,46 @@ describe("buildTaskInstanceUrl", () => { ).toBe("/dags/new_dag/runs/new_run/tasks/new_task/mapped"); }); }); + +describe("getNextHref", () => { + // Regression tests for https://github.com/apache/airflow/issues/46533 — the + // "next" parameter sent to the login redirect must be a same-origin relative + // URL so that proxied deployments (e.g. Gitpod) don't bounce the browser + // back to the API server's reported origin (e.g. http://localhost:29091). + it.each([ + { + description: "preserves pathname only", + expected: "/dags/my_dag", + input: { hash: "", pathname: "/dags/my_dag", search: "" }, + }, + { + description: "preserves pathname and search", + expected: "/dags/my_dag?tab=graph", + input: { hash: "", pathname: "/dags/my_dag", search: "?tab=graph" }, + }, + { + description: "preserves pathname, search, and hash", + expected: "/dags/my_dag?tab=graph#section", + input: { hash: "#section", pathname: "/dags/my_dag", search: "?tab=graph" }, + }, + { + description: "preserves proxied base path, search, and hash", + expected: "/team-a/dags/my_dag?tab=graph#section", + input: { hash: "#section", pathname: "/team-a/dags/my_dag", search: "?tab=graph" }, + }, + { + description: "handles root path", + expected: "/", + input: { hash: "", pathname: "/", search: "" }, + }, + ])("$description", ({ expected, input }) => { + expect(getNextHref(input)).toBe(expected); + }); + + it("does not include the origin (no http(s) prefix)", () => { + const result = getNextHref({ hash: "", pathname: "/dags/my_dag", search: "" }); + + expect(result.startsWith("http://")).toBe(false); + expect(result.startsWith("https://")).toBe(false); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/utils/links.ts b/airflow-core/src/airflow/ui/src/utils/links.ts index 6770f2e7cbb..522a72fcdda 100644 --- a/airflow-core/src/airflow/ui/src/utils/links.ts +++ b/airflow-core/src/airflow/ui/src/utils/links.ts @@ -47,6 +47,13 @@ export const getRedirectPath = (targetPath: string): string => { return new URL(targetPath, baseUrl).pathname; }; +// Build a same-origin "next" target (path + query + hash) from a Location. +// Using a relative URL ensures redirects work correctly when the UI is +// reached through a proxy or a different origin than the API server reports +// (e.g. Gitpod port-based domains, see #46533). +export const getNextHref = (location: Pick<Location, "hash" | "pathname" | "search">): string => + `${location.pathname}${location.search}${location.hash}`; + export const getTaskInstanceAdditionalPath = (pathname: string): string => { const subRoutes = taskInstanceRoutes.filter((route) => route.path !== undefined).map((route) => route.path); // Look for patterns like /tasks/{taskId}/mapped/{mapIndex}/{sub-route}
