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

pierrejeambrun 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 ac82cecf144 Fix 500 error for event logs with NULL dttm (#68338)
ac82cecf144 is described below

commit ac82cecf1445e6e90c15490fdc64eebc77771e0d
Author: Vasu Madaan <[email protected]>
AuthorDate: Wed Jun 10 20:17:30 2026 +0530

    Fix 500 error for event logs with NULL dttm (#68338)
    
    * Fix 500 error when event log has null dttm in API responses
    
    Logs with dttm=NULL cause a Pydantic validation error (500) because
    EventLogResponse.dttm is non-optional. Filter them out at the query
    level: GET /eventLogs/{id} returns 404, GET /eventLogs excludes them
    from results and total_entries.
    
    closes: #68333
    
    * Fix PT028 ruff lint: add noqa for provide_session sentinel pattern
    
    * Apply ruff-format to test_event_logs.py
    
    * Add comments explaining dttm NULL filter and why response model stays 
non-optional
---
 .../core_api/routes/public/event_logs.py           | 22 ++++++++++++--
 .../core_api/routes/public/test_event_logs.py      | 35 ++++++++++++++++++++++
 2 files changed, 55 insertions(+), 2 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py
index 5088fb6f6b9..5a8b4ab9092 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/event_logs.py
@@ -64,7 +64,15 @@ def get_event_log(
     session: SessionDep,
 ) -> EventLogResponse:
     event_log = session.scalar(
-        select(Log).where(Log.id == 
event_log_id).options(joinedload(Log.task_instance))
+        # Log.dttm is nullable at the DB level, but EventLogResponse.when is a 
non-optional
+        # datetime. Rows with dttm=NULL would cause a Pydantic validation 
error (500), so
+        # exclude them here. Such rows can exist in legacy installs or via 
direct DB inserts
+        # that bypass Log.__init__ (which always sets dttm = 
timezone.utcnow()).
+        # Making EventLogResponse.when nullable would be a breaking API 
contract change for
+        # clients that currently rely on `when` always being present.
+        select(Log)
+        .where(Log.id == event_log_id, Log.dttm.is_not(None))
+        .options(joinedload(Log.task_instance))
     )
     if event_log is None:
         raise HTTPException(status.HTTP_404_NOT_FOUND, f"The Event Log with 
id: `{event_log_id}` not found")
@@ -155,7 +163,17 @@ def get_event_logs(
     readable_event_logs_filter: ReadableEventLogsFilterDep,
 ) -> EventLogCollectionResponse:
     """Get all Event Logs."""
-    query = select(Log).options(joinedload(Log.task_instance), 
joinedload(Log.dag_model))
+    query = (
+        # Log.dttm is nullable at the DB level, but EventLogResponse.when is a 
non-optional
+        # datetime. Rows with dttm=NULL would cause a Pydantic validation 
error (500), so
+        # exclude them here. Such rows can exist in legacy installs or via 
direct DB inserts
+        # that bypass Log.__init__ (which always sets dttm = 
timezone.utcnow()).
+        # Making EventLogResponse.when nullable would be a breaking API 
contract change for
+        # clients that currently rely on `when` always being present.
+        select(Log)
+        .where(Log.dttm.is_not(None))
+        .options(joinedload(Log.task_instance), joinedload(Log.dag_model))
+    )
     event_logs_select, total_entries = paginated_select(
         statement=query,
         order_by=order_by,
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_event_logs.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_event_logs.py
index 00d6918673d..eda0926ca71 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_event_logs.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_event_logs.py
@@ -50,6 +50,7 @@ EVENT_NORMAL = "NORMAL_EVENT"
 EVENT_WITH_OWNER = "EVENT_WITH_OWNER"
 EVENT_WITH_TASK_INSTANCE = "EVENT_WITH_TASK_INSTANCE"
 EVENT_WITH_OWNER_AND_TASK_INSTANCE = "EVENT_WITH_OWNER_AND_TASK_INSTANCE"
+EVENT_WITHOUT_DTTM = "EVENT_WITHOUT_DTTM"
 EVENT_NON_EXISTED_ID = 9999
 
 
@@ -233,6 +234,19 @@ class TestGetEventLog(TestEventLogsEndpoint):
             user=mock.ANY,
         )
 
+    @provide_session
+    def test_should_return_404_for_log_without_dttm(self, test_client, *, 
session: Session = NEW_SESSION):  # noqa: PT028
+        event_log = Log(event=EVENT_WITHOUT_DTTM)
+        session.add(event_log)
+        session.flush()
+        event_log_id = event_log.id
+        event_log.dttm = None
+        session.commit()
+
+        response = test_client.get(f"/eventLogs/{event_log_id}")
+
+        assert response.status_code == 404
+
 
 class TestGetEventLogs(TestEventLogsEndpoint):
     @pytest.mark.parametrize(
@@ -350,6 +364,27 @@ class TestGetEventLogs(TestEventLogsEndpoint):
         for event_log, expected_event in zip(resp_json["event_logs"], 
expected_events):
             assert event_log["event"] == expected_event
 
+    @provide_session
+    def test_get_event_logs_excludes_logs_without_dttm(
+        self,
+        test_client,
+        *,
+        session: Session = NEW_SESSION,  # noqa: PT028
+    ):
+        event_log = Log(event=EVENT_WITHOUT_DTTM)
+        session.add(event_log)
+        session.flush()
+        event_log.dttm = None
+        session.commit()
+
+        with assert_queries_count(3):
+            response = test_client.get("/eventLogs", params={"order_by": 
"-when"})
+
+        assert response.status_code == 200
+        resp_json = response.json()
+        assert resp_json["total_entries"] == 4
+        assert EVENT_WITHOUT_DTTM not in {event_log["event"] for event_log in 
resp_json["event_logs"]}
+
     # Ordering of nulls values is DB specific.
     @pytest.mark.backend("sqlite")
     @pytest.mark.parametrize(

Reply via email to