Repository: apache/airflow

Issue:
TaskFlow AirflowSkipException causes downstream step to fail
### Apache Airflow version

2.3.2 (latest released)

### What happened

Using TaskFlow API and have 2 tasks that lead to the same downstream task.  These tasks check for new data and when found will set an XCom entry of the new filename for the downstream to handle.  If no data is found the upstream tasks raise a skip exception. 
 The downstream task has the trigger_rule = none_failed_min_one_success.  

Problem is that a task which is set to Skip doesn't set any XCom.  When the downstream task starts it raises the error:
`airflow.exceptions.AirflowException: XComArg result from task2 at airflow_2_3_xcomarg_render_error with key="return_value" is not found!`

### What you think should happen instead

Based on trigger rule of "none_failed_min_one_success", expectation is that an upstream task should be allowed to skip and the downstream task will still run.  While the downstream does try to start based on trigger rules, it never really gets to run since the error is raised when rendering the arguments.

### How to reproduce

Example dag will generate the error if run.

```
from airflow.decorators import dag, task
from airflow.exceptions import AirflowSkipException

@task
def task1():
    return "example.csv"

@task
def task2():
    raise AirflowSkipException()

@task(trigger_rule="none_failed_min_one_success")
def downstream_task(t1, t2):
    print("task ran")

@dag(
    default_args={"owner": "Airflow", "start_date": "2022-06-07"},
    schedule_interval=None,
)
def airflow_2_3_xcomarg_render_error():
    t1 = task1()
    t2 = task2()
    downstream_task(t1, t2)

example_dag = airflow_2_3_xcomarg_render_error()
```

### Operating System

Ubuntu 20.04.4 LTS

### Versions of Apache Airflow Providers

_No response_

### Deployment

Virtualenv installation

### Deployment details

_No response_

### Anything else

_No response_

### Are you willing to submit PR?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
The same issue has been bothering me for a while now and preventing me from upgrading to the latest airflow version.
As a workaround, you can remove t2 from downstream_task args and check if task2 result exists in the function body. Or you can add one more task between task2 and downstream_task that will check if upstream skipped and return None or return upstream value.

I think downstream_task should be run in your case, but I'm not sure want value we expect to see in t2? None/skip exception/some_special_value_for_that_case?

Resolving patch:
diff --git a/airflow/exceptions.py b/airflow/exceptions.py
--- a/airflow/exceptions.py
+++ b/airflow/exceptions.py
@@ -91,6 +91,19 @@ class AirflowOptionalProviderFeatureException(AirflowException):
     """Raise by providers when imports are missing for optional provider features."""
 
 
+class XComNotFound(AirflowException):
+    """Raise when an XCom reference is being resolved against a non-existent XCom."""
+
+    def __init__(self, dag_id: str, task_id: str, key: str) -> None:
+        super().__init__()
+        self.dag_id = dag_id
+        self.task_id = task_id
+        self.key = key
+
+    def __str__(self) -> str:
+        return f'XComArg result from {self.task_id} at {self.dag_id} with key="{self.key}" is not found!'
+
+
 class UnmappableOperator(AirflowException):
     """Raise when an operator is not implemented to be mappable."""
 
diff --git a/airflow/models/xcom_arg.py b/airflow/models/xcom_arg.py
--- a/airflow/models/xcom_arg.py
+++ b/airflow/models/xcom_arg.py
@@ -35,7 +35,7 @@
 from sqlalchemy import func
 from sqlalchemy.orm import Session
 
-from airflow.exceptions import AirflowException
+from airflow.exceptions import XComNotFound
 from airflow.models.abstractoperator import AbstractOperator
 from airflow.models.taskmixin import DAGNode, DependencyMixin
 from airflow.models.xcom import XCOM_RETURN_KEY
@@ -199,6 +199,12 @@ def get_task_map_length(self, run_id: str, *, session: "Session") -> Optional[in
         raise NotImplementedError()
 
     def resolve(self, context: Context, session: "Session" = NEW_SESSION) -> Any:
+        """Pull XCom value.
+
+        This should only be called during ``op.execute()`` in respectable context.
+
+        :meta private:
+        """
         raise NotImplementedError()
 
 
@@ -320,19 +326,13 @@ def get_task_map_length(self, run_id: str, *, session: "Session") -> Optional[in
 
     @provide_session
     def resolve(self, context: Context, session: "Session" = NEW_SESSION) -> Any:
-        """
-        Pull XCom value for the existing arg. This method is run during ``op.execute()``
-        in respectable context.
-        """
-        result = context["ti"].xcom_pull(
-            task_ids=self.operator.task_id, key=str(self.key), default=NOTSET, session=session
-        )
-        if result is NOTSET:
-            raise AirflowException(
-                f'XComArg result from {self.operator.task_id} at {context["ti"].dag_id} '
-                f'with key="{self.key}" is not found!'
-            )
-        return result
+        task_id = self.operator.task_id
+        result = context["ti"].xcom_pull(task_ids=task_id, key=str(self.key), default=NOTSET, session=session)
+        if result is not NOTSET:
+            return result
+        if self.key == XCOM_RETURN_KEY:
+            return None
+        raise XComNotFound(context["ti"].dag_id, task_id, self.key)
 
 
 class _MapResult(Sequence):


Test patch:
diff --git a/tests/decorators/test_python.py b/tests/decorators/test_python.py
--- a/tests/decorators/test_python.py
+++ b/tests/decorators/test_python.py
@@ -759,3 +759,40 @@ def org_test_func():
         decorated_test_func, '__wrapped__'
     ), "decorated function does not have __wrapped__ attribute"
     assert decorated_test_func.__wrapped__ is org_test_func, "__wrapped__ attr is not the original function"
+
+
+def test_upstream_exception_produces_none_xcom(dag_maker, session):
+    from airflow.exceptions import AirflowSkipException
+    from airflow.models.dagrun import DagRun
+    from airflow.utils.trigger_rule import TriggerRule
+
+    result = None
+
+    with dag_maker(session=session) as dag:
+
+        @dag.task()
+        def up1() -> str:
+            return "example"
+
+        @dag.task()
+        def up2() -> None:
+            raise AirflowSkipException()
+
+        @dag.task(trigger_rule=TriggerRule.NONE_FAILED_MIN_ONE_SUCCESS)
+        def down(a, b):
+            nonlocal result
+            result = f"{a!r} {b!r}"
+
+        down(up1(), up2())
+
+    dr: DagRun = dag_maker.create_dagrun()
+
+    decision = dr.task_instance_scheduling_decisions(session=session)
+    assert len(decision.schedulable_tis) == 2  # "up1" and "up2"
+    for ti in decision.schedulable_tis:
+        ti.run(session=session)
+
+    decision = dr.task_instance_scheduling_decisions(session=session)
+    assert len(decision.schedulable_tis) == 1  # "down"
+    decision.schedulable_tis[0].run(session=session)
+    assert result == "'example' None"