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

(airflow) branch v3-0-test updated: Fix pushing None as an XCom value (#55080) (#55095)

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

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


The following commit(s) were added to refs/heads/v3-0-test by this push:
     new 3b8ba3b355a Fix pushing None as an XCom value (#55080) (#55095)
3b8ba3b355a is described below

commit 3b8ba3b355a9aa0316e7fc74e5d25602713ec1a9
Author: Jens Scheffler <95...@users.noreply.github.com>
AuthorDate: Sat Aug 30 13:35:56 2025 +0200

    Fix pushing None as an XCom value (#55080) (#55095)
    
    fixes #55062
    
    (cherry picked from commit b4cf3afe2ab6b724f4261a20da31c61410acb93a)
    
    Co-authored-by: Kalyan R <ka...@live.com>
---
 .../src/airflow/api_fastapi/execution_api/routes/xcoms.py      |  4 ++--
 airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx       | 10 +++++++---
 .../unit/api_fastapi/execution_api/versions/head/test_xcoms.py | 10 +++++++---
 3 files changed, 16 insertions(+), 8 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 eae3539f593..332591d2d1d 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
@@ -333,6 +333,7 @@ def set_xcom(
     run_id: str,
     task_id: str,
     key: str,
+    session: SessionDep,
     value: Annotated[
         JsonValue,
         Body(
@@ -352,8 +353,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 342a0a4e909..c349c8b70dc 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
@@ -39,6 +39,10 @@ export const XComEntry = ({ dagId, mapIndex, runId, taskId, xcomKey }: XComEntry
     taskId,
     xcomKey,
   });
+  // When deserialize=true, the API returns a stringified representation
+  // so we don't need to JSON.stringify it again
+  const xcomValue = data?.value;
+  const valueFormatted = typeof xcomValue === "string" ? xcomValue : JSON.stringify(xcomValue, undefined, 4);
 
   return isLoading ? (
     <Skeleton
@@ -50,11 +54,11 @@ export const XComEntry = ({ dagId, mapIndex, runId, taskId, xcomKey }: XComEntry
   ) : (data?.value?.length ?? 0) > 0 ? (
     <HStack>
       <Text>{data?.value}</Text>
-      {Boolean(data?.value) ? (
-        <ClipboardRoot value={data?.value ?? ""}>
+      {xcomValue === undefined || xcomValue === null ? undefined : (
+        <ClipboardRoot value={valueFormatted}>
           <ClipboardIconButton />
         </ClipboardRoot>
-      ) : undefined}
+      )}
     </HStack>
   ) : undefined;
 };
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 2403e649cbf..b5811d91992 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()