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")