This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun 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 bdf3ee57032 Prevent React plugins from inheriting a prior bundle's
component (#71943)
bdf3ee57032 is described below
commit bdf3ee57032da6934d27ff8302114b730d849c56
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Fri Aug 21 17:35:31 2026 +0200
Prevent React plugins from inheriting a prior bundle's component (#71943)
A React plugin bundle exports its component by assigning to the shared
globalThis.AirflowPlugin as a side effect of being imported. When a bundle
is
malformed or empty and never makes that assignment, the loader read whatever
value a previously-loaded plugin had left there, so one plugin silently
rendered another plugin's component instead of failing. Clear the shared
global
before each import so a bundle that assigns nothing surfaces a clear error
rather than inheriting a stale component.
---
.../src/airflow/ui/src/pages/ReactPlugin.test.tsx | 26 +++++++++++++++++++++-
.../src/airflow/ui/src/pages/ReactPlugin.tsx | 10 +++++++--
2 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx
b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx
index 7f0fb6d5609..ea27f6c90c8 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx
@@ -30,7 +30,7 @@ import type {
} from "openapi/requests/types.gen";
import { Wrapper } from "src/utils/Wrapper";
-import { ReactPlugin, type PluginProps } from "./ReactPlugin";
+import { loadPlugin, ReactPlugin, type PluginProps } from "./ReactPlugin";
const mockAsset = { id: 1, name: "my_asset", uri: "s3://bucket/key" } as
AssetResponse;
const mockDag = { dag_display_name: "My Dag", dag_id: "my_dag" } as
DAGDetailsResponse;
@@ -142,3 +142,27 @@ describe("ReactPlugin context props", () => {
expect(capturedProps?.assetUri).toBe(mockAsset.uri);
});
});
+
+const pluginA = { bundle_url: "http://localhost/a.js", name: "PluginA" } as
ReactAppResponse;
+const pluginB = { bundle_url: "http://localhost/b.js", name: "PluginB" } as
ReactAppResponse;
+const componentA = () => null;
+
+describe("loadPlugin", () => {
+ afterEach(() => {
+ for (const key of ["AirflowPlugin", pluginA.name, pluginB.name]) {
+ (globalThis as Record<string, unknown>)[key] = undefined;
+ }
+ });
+
+ it("does not let a malformed bundle inherit a previously-loaded plugin's
component", async () => {
+ // A well-formed bundle sets globalThis.AirflowPlugin on import; a
malformed one sets nothing.
+ await loadPlugin(pluginA, () => {
+ (globalThis as Record<string, unknown>).AirflowPlugin = componentA;
+
+ return Promise.resolve();
+ });
+ const { default: component } = await loadPlugin(pluginB, () =>
Promise.resolve());
+
+ expect(component).not.toBe(componentA);
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
index a14fdcb13d3..9811aa7a85f 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
@@ -51,9 +51,14 @@ export type PluginProps = {
type PluginComponentType = FC<PluginProps>;
-const loadPlugin = (reactApp: ReactAppResponse): Promise<{ default:
PluginComponentType }> =>
+export const loadPlugin = (
+ reactApp: ReactAppResponse,
+ importBundle: (url: string) => Promise<unknown> = (url) => import(/*
@vite-ignore */ url),
+): Promise<{ default: PluginComponentType }> => {
+ (globalThis as Record<string, unknown>).AirflowPlugin = undefined;
+
// We are assuming the plugin manager is trusted and the bundle_url is safe
- import(/* @vite-ignore */ new URL(reactApp.bundle_url,
document.baseURI).href)
+ return importBundle(new URL(reactApp.bundle_url, document.baseURI).href)
.then(() => {
// Store components in globalThis[reactApp.name] to avoid conflicts with
the shared globalThis.AirflowPlugin
// global variable.
@@ -78,6 +83,7 @@ const loadPlugin = (reactApp: ReactAppResponse): Promise<{
default: PluginCompon
return { default: ErrorPage };
});
+};
export const ReactPlugin = ({ reactApp }: { readonly reactApp:
ReactAppResponse }) => {
const { assetId, dagId, mapIndex, runId, taskId } = useParams();