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

weilee pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 5619bf196e7 fix(hitl): Handle task instance clearing cases when 
getting and updating an existing HITLDetail (#53863)
5619bf196e7 is described below

commit 5619bf196e7b2103b9a410cafaa3395756fac9f8
Author: Wei Lee <[email protected]>
AuthorDate: Wed Jul 30 15:29:55 2025 +0800

    fix(hitl): Handle task instance clearing cases when getting and updating an 
existing HITLDetail (#53863)
---
 .../api_fastapi/execution_api/routes/hitl.py       | 16 +++++++++++
 .../execution_api/versions/head/test_hitl.py       | 31 ++++++++++++++++++++++
 2 files changed, 47 insertions(+)

diff --git a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py
index 36d50b19fa7..9a34392f3ad 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/routes/hitl.py
@@ -85,6 +85,20 @@ def upsert_hitl_detail(
     return HITLDetailRequest.model_validate(hitl_detail_model)
 
 
+def _check_hitl_detail_exists(hitl_detail_model: HITLDetail) -> None:
+    if not hitl_detail_model:
+        raise HTTPException(
+            status.HTTP_404_NOT_FOUND,
+            detail={
+                "reason": "not_found",
+                "message": (
+                    "HITLDetail not found. "
+                    "This happens most likely due to clearing task instance 
before receiving response."
+                ),
+            },
+        )
+
+
 @router.patch("/{task_instance_id}")
 def update_hitl_detail(
     task_instance_id: UUID,
@@ -94,6 +108,7 @@ def update_hitl_detail(
     """Update the response part of a Human-in-the-loop detail for a specific 
Task Instance."""
     ti_id_str = str(task_instance_id)
     hitl_detail_model = 
session.execute(select(HITLDetail).where(HITLDetail.ti_id == 
ti_id_str)).scalar()
+    _check_hitl_detail_exists(hitl_detail_model)
     if hitl_detail_model.response_received:
         raise HTTPException(
             status.HTTP_409_CONFLICT,
@@ -122,4 +137,5 @@ def get_hitl_detail(
     hitl_detail_model = session.execute(
         select(HITLDetail).where(HITLDetail.ti_id == ti_id_str),
     ).scalar()
+    _check_hitl_detail_exists(hitl_detail_model)
     return HITLDetailResponse.from_hitl_detail_orm(hitl_detail_model)
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py
index 90c6d6350db..264b0dba319 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/head/test_hitl.py
@@ -28,6 +28,7 @@ from datetime import datetime
 from typing import TYPE_CHECKING, Any
 
 import time_machine
+from uuid6 import uuid7
 
 from airflow._shared.timezones.timezone import convert_to_utc
 from airflow.models.hitl import HITLDetail
@@ -155,8 +156,38 @@ def test_update_hitl_detail(client: Client, sample_ti: 
TaskInstance) -> None:
     }
 
 
+def test_update_hitl_detail_without_ti(client: Client) -> None:
+    ti_id = str(uuid7())
+    response = client.patch(
+        f"/execution/hitl-details/{ti_id}",
+        json={
+            "ti_id": ti_id,
+            "chosen_options": ["Reject"],
+            "params_input": {"input_1": 2},
+        },
+    )
+    assert response.status_code == 404
+    assert response.json() == {
+        "detail": {
+            "message": "HITLDetail not found. This happens most likely due to 
clearing task instance before receiving response.",
+            "reason": "not_found",
+        },
+    }
+
+
 @pytest.mark.usefixtures("sample_hitl_detail")
 def test_get_hitl_detail(client: Client, sample_ti: TaskInstance) -> None:
     response = client.get(f"/execution/hitlDetails/{sample_ti.id}")
     assert response.status_code == 200
     assert response.json() == expected_empty_hitl_detail_response_part
+
+
+def test_get_hitl_detail_without_ti(client: Client) -> None:
+    response = client.get(f"/execution/hitl-details/{uuid7()}")
+    assert response.status_code == 404
+    assert response.json() == {
+        "detail": {
+            "message": "HITLDetail not found. This happens most likely due to 
clearing task instance before receiving response.",
+            "reason": "not_found",
+        },
+    }

Reply via email to