Skip to content

UI: API requests bypass <base href> during startup, breaking deployments served under a path prefix #72344

Description

@edsu

Under which category would you file this issue?

Airflow Core

Apache Airflow version

3.3.1

What happened and how to reproduce it?

When Airflow is served under a path prefix ([api] base_url = http://host/workflows), the UI
issues its first API request to the origin root instead of the configured prefix:

GET /api/v2/version        <- actual
GET /workflows/api/v2/version   <- expected

Every later request is correctly prefixed; only the startup one is wrong.

On a deployment where something else is mounted at /api/, that request reaches the wrong
service. In our case a reverse proxy routes /api/ to a different application, which answers
401. The UI treats the 401 as "session expired" and redirects to login; login succeeds, the
app reloads, the same request 401s again. The result is an infinite login redirect loop
the UI renders its frame, data loads correctly, then it bounces back to login, about once per
second.

Even where /api/ is not otherwise routed, the request is still sent to the wrong path.

This problem is not present in 3.2.0.

Root cause

OpenAPI.BASE is assigned as a module side effect in queryClient.ts:

// airflow-core/src/airflow/ui/src/queryClient.ts
import i18n from "src/i18n/config";                                    // line 23
...
OpenAPI.BASE = document.querySelector("head>base")?.getAttribute("href") ?? "";   // line 27

But queryClient.ts imports src/i18n/config, and that module requests the version at
module scope:

// airflow-core/src/airflow/ui/src/i18n/config.ts
export const resolveI18nVersion = (): Promise<string> =>
  VersionService.getVersion()
    .then((data) => data.version)
    .catch(() => Date.now().toString());

void resolveI18nVersion().then(initI18n);                              // line 163

ES module evaluation runs a dependency to completion before the importing module's body, so
i18n/config.ts always issues getVersion() before line 27 assigns OpenAPI.BASE. The
generated client builds the URL as config.BASE + path with BASE still "".

A visible side effect: because the request fails, resolveI18nVersion falls back to
Date.now(), so translation files are fetched with a timestamp cache buster
(?v=1787955819428) rather than the version. The cache-busting feature the call exists for is
therefore also defeated on these deployments.

Regression

Introduced by #65720 ("i18n translation files served stale after Airflow upgrade due to
browser cache"), which added the getVersion() call to i18n/config.ts. That commit is not in
3.2.0 and is in 3.3.0+. In 3.2.0 i18n/config.ts neither imports VersionService nor calls
the API, so the first request happened after OpenAPI.BASE was set.

Note there are now three separate <base href> lookups in the UI
(queryClient.ts, i18n/config.ts, utils/links.ts), which is what let these drift apart.

How to reproduce

Unit test (fails on main):

// airflow-core/src/airflow/ui/src/queryClient.test.ts
const BASE_HREF = "/workflows/";

vi.resetModules();
document.head.innerHTML = `<base href="${BASE_HREF}" />`;

const requested: Array<string> = [];
vi.spyOn(axios, "request").mockImplementation(async (config) => {
  requested.push(String(config.url));
  return { config, data: { git_version: null, version: "3.3.1" }, headers: {}, status: 200, statusText: "OK" };
});

await import("src/queryClient");
await vi.waitFor(() => expect(requested.length).toBeGreaterThan(0));

expect(requested.filter((url) => !url.startsWith(BASE_HREF))).toEqual([]);
AssertionError: expected [ '/api/v2/version' ] to deeply equal []

What you think should happen instead?

All API requests, including any issued during module initialization, resolve against
<base href> so that a prefix deployment never sends requests to the origin root.

Operating System

macOS 26.6.2

Deployment

Docker-Compose

Apache Airflow Provider(s)

No response

Versions of Apache Airflow Providers

No response

Official Helm Chart version

Not Applicable

Kubernetes Version

No response

Helm Chart configuration

No response

Docker Image customizations

https://github.com/blue-core-lod/bluecore-workflows/blob/upgrade-airflow-3.3.1/Dockerfile

Anything else?

No response

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:bugThis is a clearly a bugneeds-triagelabel for new issues that we didn't triage yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions