This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-2-test by this push:
     new f7bb447214b UI: Preserve proxied URL on login redirect (#66690) 
(#67091)
f7bb447214b is described below

commit f7bb447214b342fb75ddbf609b1c5c53d72c7fdc
Author: Rahul Vats <[email protected]>
AuthorDate: Mon May 18 15:09:45 2026 +0530

    UI: Preserve proxied URL on login redirect (#66690) (#67091)
    
    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)
    
    Co-authored-by: Sai Teja Desu 
<[email protected]>
---
 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}

Reply via email to