You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ka...@apache.org on 2025/08/30 08:43:18 UTC

(airflow) branch main updated: Fix pushing None as an XCom value (#55080)

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

kaxilnaik 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 b4cf3afe2ab Fix pushing None as an XCom value (#55080)
b4cf3afe2ab is described below

commit b4cf3afe2ab6b724f4261a20da31c61410acb93a
Author: Kalyan R <ka...@live.com>
AuthorDate: Sat Aug 30 14:13:11 2025 +0530

    Fix pushing None as an XCom value (#55080)
    
    fixes #55062
---
 .../src/airflow/api_fastapi/execution_api/routes/xcoms.py   |  4 ++--
 airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx    | 13 +++++++------
 .../api_fastapi/execution_api/versions/head/test_xcoms.py   | 10 +++++++---
 3 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py
index 230e9837de1..576f6c4e0dc 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/xcoms.py
@@ -326,6 +326,7 @@ def set_xcom(
     run_id: str,
     task_id: str,
     key: Annotated[str, StringConstraints(min_length=1)],
+    session: SessionDep,
     value: Annotated[
         JsonValue,
         Body(
@@ -345,8 +346,7 @@ def set_xcom(
                 },
             },
         ),
-    ],
-    session: SessionDep,
+    ] = None,
     map_index: Annotated[int, Query()] = -1,
     mapped_length: Annotated[
         int | None, Query(description="Number of mapped tasks this value expands into")
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
index 510397ac607..05fbd4c2c1a 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
@@ -74,8 +74,9 @@ export const XComEntry = ({ dagId, mapIndex, runId, taskId, xcomKey }: XComEntry
   });
   // When deserialize=true, the API returns a stringified representation
   // so we don't need to JSON.stringify it again
-  const valueFormatted =
-    typeof data?.value === "string" ? data.value : JSON.stringify(data?.value, undefined, 4);
+  const xcomValue = data?.value;
+  const isObjectOrArray = Array.isArray(xcomValue) || (xcomValue !== null && typeof xcomValue === "object");
+  const valueFormatted = typeof xcomValue === "string" ? xcomValue : JSON.stringify(xcomValue, undefined, 4);
 
   return isLoading ? (
     <Skeleton
@@ -86,16 +87,16 @@ export const XComEntry = ({ dagId, mapIndex, runId, taskId, xcomKey }: XComEntry
     />
   ) : (
     <HStack>
-      {["array", "object"].includes(typeof data?.value) ? (
-        <RenderedJsonField content={data?.value as object} enableClipboard={false} />
+      {isObjectOrArray ? (
+        <RenderedJsonField content={xcomValue as object} enableClipboard={false} />
       ) : (
         <Text>{renderTextWithLinks(valueFormatted)}</Text>
       )}
-      {Boolean(data?.value) ? (
+      {xcomValue === undefined || xcomValue === null ? undefined : (
         <ClipboardRoot value={valueFormatted}>
           <ClipboardIconButton />
         </ClipboardRoot>
-      ) : undefined}
+      )}
     </HStack>
   );
 };
diff --git a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py
index 0841a76ed1a..f9408c808f2 100644
--- a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py
+++ b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_xcoms.py
@@ -331,13 +331,17 @@ class TestXComsSetEndpoint:
             ('{"key2": "value2"}', '{"key2": "value2"}'),
             ('{"key2": "value2", "key3": ["value3"]}', '{"key2": "value2", "key3": ["value3"]}'),
             ('["value1"]', '["value1"]'),
+            (None, None),
         ],
     )
     def test_xcom_set(self, client, create_task_instance, session, value, expected_value):
         """
-        Test that XCom value is set correctly. The value is passed as a JSON string in the request body.
-        XCom.set then uses json.dumps to serialize it and store the value in the database.
-        This is done so that Task SDK in multiple languages can use the same API to set XCom values.
+        Test that XCom value is set correctly. The request body can be either:
+        - a JSON string (e.g. '"value"', '{"k":"v"}', '[1]'), which is stored as-is (a string) in the DB
+        - the JSON literal null, which is stored as a None
+
+        This mirrors the Execution API contract where the body is the JSON value itself; Task SDKs may send
+        pre-serialized JSON strings or null.
         """
         ti = create_task_instance()
         session.commit()