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

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


The following commit(s) were added to refs/heads/v3-2-test by this push:
     new f99b279923a [v3-2-test] Fix TypeError in GET /dags/{dag_id}/tasks when 
order_by field has None values (#64384) (#64587)
f99b279923a is described below

commit f99b279923a1d9f0d294e92bd8378285f1b86a73
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Apr 8 11:31:18 2026 +0200

    [v3-2-test] Fix TypeError in GET /dags/{dag_id}/tasks when order_by field 
has None values (#64384) (#64587)
    
    The tasks endpoint crashed with a 500 Internal Server Error when sorting
    by a field (e.g. start_date) that contains None values, because Python 3
    cannot compare None with None using '<'.
    
    This adds explicit validation of the order_by parameter against a
    whitelist of sortable fields (returning 400 for invalid fields, consistent
    with SortParam used in other endpoints) and handles None values in the
    sort key so nullable fields work correctly.
    
    Closes: #63927
    (cherry picked from commit 15cf396a3b906f9297852d6a4cb9e36dbdb43ef1)
    
    Co-authored-by: Antonio Mello <[email protected]>
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../api_fastapi/core_api/routes/public/tasks.py    | 40 +++++++++++++++++++---
 .../core_api/routes/public/test_tasks.py           | 16 +++++++--
 2 files changed, 49 insertions(+), 7 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/tasks.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/tasks.py
index c548989835d..2df27b682ea 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/tasks.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/tasks.py
@@ -17,7 +17,6 @@
 
 from __future__ import annotations
 
-from operator import attrgetter
 from typing import cast
 
 from fastapi import Depends, HTTPException, status
@@ -33,6 +32,29 @@ from airflow.exceptions import TaskNotFound
 
 tasks_router = AirflowRouter(tags=["Task"], prefix="/dags/{dag_id}/tasks")
 
+_SORTABLE_TASK_FIELDS = {
+    "task_id",
+    "task_display_name",
+    "owner",
+    "start_date",
+    "end_date",
+    "trigger_rule",
+    "depends_on_past",
+    "wait_for_downstream",
+    "retries",
+    "queue",
+    "pool",
+    "pool_slots",
+    "execution_timeout",
+    "retry_delay",
+    "retry_exponential_backoff",
+    "priority_weight",
+    "weight_rule",
+    "ui_color",
+    "ui_fgcolor",
+    "operator_name",
+}
+
 
 @tasks_router.get(
     "",
@@ -52,10 +74,18 @@ def get_tasks(
 ) -> TaskCollectionResponse:
     """Get tasks for DAG."""
     dag = get_latest_version_of_dag(dag_bag, dag_id, session)
-    try:
-        tasks = sorted(dag.tasks, key=attrgetter(order_by.lstrip("-")), 
reverse=(order_by[0:1] == "-"))
-    except AttributeError as err:
-        raise HTTPException(status.HTTP_400_BAD_REQUEST, str(err))
+    lstripped_order_by = order_by.lstrip("-")
+    if lstripped_order_by not in _SORTABLE_TASK_FIELDS:
+        raise HTTPException(
+            status.HTTP_400_BAD_REQUEST,
+            f"Ordering with '{lstripped_order_by}' is disallowed or "
+            f"the attribute does not exist on the model",
+        )
+    tasks = sorted(
+        dag.tasks,
+        key=lambda task: (getattr(task, lstripped_order_by) is None, 
getattr(task, lstripped_order_by)),
+        reverse=(order_by[0:1] == "-"),
+    )
     return TaskCollectionResponse(
         tasks=cast("list[TaskResponse]", tasks),
         total_entries=len(tasks),
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py
index dd85ad1c325..0dacd19397e 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_tasks.py
@@ -543,10 +543,22 @@ class TestGetTasks(TestTaskEndpoint):
             
f"{self.api_prefix}/{self.dag_id}/tasks?order_by=invalid_task_colume_name",
         )
         assert response.status_code == 400
-        assert (
-            response.json()["detail"] == "'EmptyOperator' object has no 
attribute 'invalid_task_colume_name'"
+        assert response.json()["detail"] == (
+            "Ordering with 'invalid_task_colume_name' is disallowed or "
+            "the attribute does not exist on the model"
         )
 
+    def test_should_respond_200_order_by_start_date_with_none(self, 
test_client):
+        """Sorting by a nullable field should not raise TypeError (issue 
#63927)."""
+        response = test_client.get(
+            
f"{self.api_prefix}/{self.unscheduled_dag_id}/tasks?order_by=start_date",
+        )
+        assert response.status_code == 200
+        tasks = response.json()["tasks"]
+        assert len(tasks) == 2
+        # All start_dates are None for unscheduled tasks; verify they sort 
without error
+        assert all(t["start_date"] is None for t in tasks)
+
     def test_should_respond_404(self, test_client):
         dag_id = "xxxx_not_existing"
         response = test_client.get(f"{self.api_prefix}/{dag_id}/tasks")

Reply via email to