paultmathew opened a new issue, #66428:
URL: https://github.com/apache/airflow/issues/66428

   ### Apache Airflow version
   
   3.2.0 (also reproduced on `main` and `v3-2-stable` by source inspection)
   
   ### What happened?
   
   Any HITL task instance with `map_index >= 0` (i.e. produced by a mapped task 
or a task inside a mapped `@task_group` via `expand_kwargs` / `expand`) is 
silently invisible on every Required Actions listing view in the UI:
   
   - `Browse → Required Actions` (`/required_actions`)
   - Per-DAG `Required Actions` tab (`/dags/<dag_id>/required_actions`)
   - Per-Run `Required Actions` tab 
(`/dags/<dag_id>/runs/<run_id>/required_actions`)
   - Per-Task `Required Actions` tab 
(`/dags/<dag_id>/tasks/<task_id>/required_actions`)
   
   The home-page "pending required actions" badge counts the row correctly (its 
SQL — `responded_at IS NULL AND state = DEFERRED` in 
`airflow/api_fastapi/common/parameters.py:774-783` — does not filter on 
`map_index`), so the user sees a count of N pending actions but the listing 
pages are empty. The per-task-instance Required Actions tab 
(`/dags/<dag_id>/runs/<run_id>/tasks/<task_id>/mapped/<map_index>/required_actions`)
 works because it uses the singular 
`/taskInstances/{task_id}/{map_index}/hitlDetails` endpoint instead of the 
listing endpoint.
   
   ### Root cause
   
   
`airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx` 
(identical in `main`, `v3-2-stable`, and `v3-1-stable`):
   
   ```tsx
   const mapIndex = searchParams.get(MAP_INDEX) ?? "-1";
   …
   const { data, error, isLoading } = useTaskInstanceServiceGetHitlDetails(
     {
       …
       mapIndex: parseInt(mapIndex, 10),
       …
     },
     …
   );
   ```
   
   When the URL has no `map_index` query param (the default for every 
entrypoint into the page), `mapIndex` is the literal string `"-1"`, then parsed 
and sent as a hard `map_index=-1` filter on the listing API call. `-1` is the 
sentinel for non-mapped task instances, so any mapped HITL row is silently 
dropped server-side.
   
   The `getQueryString` helper in 
`airflow-core/src/airflow/ui/openapi-gen/requests/core/request.ts:48` only 
skips `undefined` and `null` values, so `parseInt("-1", 10) = -1` is always 
serialized into the request URL.
   
   This regression was introduced by #55776 (`feat(hitl): add map_index filter 
to get_hitl_details endpoint`, merged 2025-09-18, shipped in 3.1.0) and is 
still present on `main` today.
   
   ### What you think should happen instead?
   
   When the URL does not contain `map_index`, the listing call should not send 
a `map_index` filter at all, so HITL rows for both mapped and unmapped tasks 
show up. The `map_index` query parameter should remain available as an opt-in 
filter for users who want to drill down to a specific map index.
   
   Suggested patch:
   
   ```tsx
   -  const mapIndex = searchParams.get(MAP_INDEX) ?? "-1";
   +  const mapIndexParam = searchParams.get(MAP_INDEX);
   …
   -      mapIndex: parseInt(mapIndex, 10),
   +      mapIndex: mapIndexParam !== null ? parseInt(mapIndexParam, 10) : 
undefined,
   ```
   
   ### How to reproduce
   
   ```python
   import pendulum
   from airflow.providers.standard.operators.hitl import ApprovalOperator
   from airflow.sdk import DAG, task_group
   
   
   with DAG(
       dag_id="hitl_mapped_repro",
       schedule=None,
       start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
   ):
       @task_group(group_id="g")
       def per_item(name: str):
           ApprovalOperator(task_id="review", subject=f"review {name}", 
fail_on_reject=True)
   
       per_item.expand(name=["a", "b", "c"])
   ```
   
   Trigger the DAG. The 3 `g.review` instances all defer (`map_index = 0, 1, 
2`). The home-page "Pending required actions" badge shows `3`. `Browse → 
Required Actions` shows "No Required Actions found". Direct API calls confirm 
the data is present and the UI default is what hides it:
   
   ```bash
   $ curl 
"$AF/api/v2/dags/~/dagRuns/~/hitlDetails?response_received=false&state=deferred"
 | jq '.total_entries'
   3
   
   $ curl 
"$AF/api/v2/dags/~/dagRuns/~/hitlDetails?response_received=false&state=deferred&map_index=-1"
 | jq '.total_entries'
   0  # ← what the UI sends by default
   
   $ curl 
"$AF/api/v2/dags/~/dagRuns/~/hitlDetails?response_received=false&state=deferred&map_index=0"
 | jq '.total_entries'
   1  # ← row IS in the DB and reachable, just hidden by the UI's default filter
   ```
   
   There is no URL value the user can set that disables the filter — the API 
parameter is strictly typed `int | None`, so empty string / `null` / `NaN` / 
wildcard values all 422; and `parseInt(string, 10)` in the UI always yields a 
number, so the OpenAPI client never produces `undefined` (the only value that 
would skip serialization in `request.ts:48`).
   
   ### Operating system
   
   Reproduced on Linux container (`apache/airflow:3.2.0-python3.12`) running 
locally via Docker Compose. Source inspection confirms the bug is identical on 
`main` and `v3-2-stable`.
   
   ### Versions of Apache Airflow Providers
   
   Standard provider with `HITLOperator` / `ApprovalOperator` (any version that 
supports HITL).
   
   ### Deployment
   
   Other Docker-based deployment (Astronomer-style local setup with 
`LocalExecutor`).
   
   ### Anything else?
   
   - The same component also has a small auto-refresh predicate bug: 
`detail.responded_at === undefined` should compare to `null` (the API 
serializes the field as JSON `null`, not omitted), so even when the listing 
query is fixed the page won't auto-refresh while waiting for new actions. Happy 
to roll that into the same PR if maintainers are OK with it.
   - The "pending required actions" badge subquery in 
`airflow/api_fastapi/common/parameters.py:774-783` does not include a 
`map_index` predicate, which is why it disagrees with the listing — that 
asymmetry is what makes the bug user-visible (UI shows "1 pending" badge but 
empty listing).
   
   ### Are you willing to submit PR?
   
   - [x] 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)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to