From 2000814178e424629ad5a76e11300367cf27b92b Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Tue, 21 Apr 2026 20:11:01 +0200 Subject: [PATCH] [v3-2-test] Fix airflowctl dagrun list crash when --state is omitted (#65608) DagRunOperations.list() required a non-None state and unconditionally sent str(state) to the API. When the CLI omitted --state, argparse passed None and the API received the literal string "None", failing with "Invalid value for state. Valid values are queued, running, success, failed". Make state optional (Optional[str] = None) and only include it in the query string when provided. Give limit a sensible default (100) while we are at it so the method works when called with no args. The auto-generated `airflowctl dagrun list` command now accepts --state as a true filter instead of a required flag. Reported in #65497 (rc2 testing). (cherry picked from commit 89a021cd72fd4589c0244a1127be5b193a93d6da) Co-authored-by: Jarek Potiuk --- airflow-ctl/src/airflowctl/api/operations.py | 13 ++++++------- .../tests/airflow_ctl/api/test_operations.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/airflow-ctl/src/airflowctl/api/operations.py b/airflow-ctl/src/airflowctl/api/operations.py index 3ce196c10cb32..a87683468cddf 100644 --- a/airflow-ctl/src/airflowctl/api/operations.py +++ b/airflow-ctl/src/airflowctl/api/operations.py @@ -606,8 +606,8 @@ def get(self, dag_id: str, dag_run_id: str) -> DAGRunResponse | ServerResponseEr def list( self, - state: str, - limit: int, + state: str | None = None, + limit: int = 100, start_date: datetime.datetime | None = None, end_date: datetime.datetime | None = None, dag_id: str | None = None, @@ -616,7 +616,7 @@ def list( List dag runs (at most `limit` results). Args: - state: Filter dag runs by state + state: Filter dag runs by state (optional; no filter applied when omitted) start_date: Filter dag runs by start date (optional) end_date: Filter dag runs by end date (optional) limit: Limit the number of results returned @@ -626,10 +626,9 @@ def list( if not dag_id: dag_id = "~" - params: dict[str, Any] = { - "state": str(state), - "limit": limit, - } + params: dict[str, Any] = {"limit": limit} + if state is not None: + params["state"] = str(state) if start_date is not None: params["start_date"] = start_date.isoformat() if end_date is not None: diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py index aa559f174214b..efb430ccc532c 100644 --- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py +++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py @@ -1135,6 +1135,20 @@ def handle_request(request: httpx.Request) -> httpx.Response: ) assert response == self.dag_run_collection_response + def test_list_without_state_does_not_send_state_param(self): + """`state` is optional: omitting it must not send ``state=None`` to the API.""" + captured_params: dict[str, str] = {} + + def handle_request(request: httpx.Request) -> httpx.Response: + captured_params.update(dict(request.url.params)) + return httpx.Response(200, json=json.loads(self.dag_run_collection_response.model_dump_json())) + + client = make_api_client(transport=httpx.MockTransport(handle_request)) + response = client.dag_runs.list(limit=5) + assert response == self.dag_run_collection_response + assert "state" not in captured_params + assert captured_params["limit"] == "5" + class TestJobsOperations: job_response = JobResponse(