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

ash 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 a3982018c98 Fix TypeError in parseStreamingLogContent for non-string 
data (#58314)
a3982018c98 is described below

commit a3982018c982d082a0ae553b0f0faa307ac6c9c1
Author: Felix Uellendall <[email protected]>
AuthorDate: Mon Nov 17 15:55:41 2025 +0100

    Fix TypeError in parseStreamingLogContent for non-string data (#58314)
    
    The function would fail with 'TypeError: content.split is not a function'
    when data was not a string but also didn't have a content property.
    
    Changes:
    - Add type guard to check if data is string before calling .split()
    - Add support for single log line objects (return as array)
    - Simplify logic with early returns for clearer flow
    - Add comprehensive test suite covering all edge cases
    
    The function now handles:
    1. Data with content property (returns as-is)
    2. String data (parses as newline-separated JSON)
    3. Object data without content (returns wrapped in array)
    4. Edge cases (undefined, null, numbers, invalid JSON)
---
 airflow-core/src/airflow/ui/src/utils/logs.test.ts | 108 +++++++++++++++++++++
 airflow-core/src/airflow/ui/src/utils/logs.ts      |  15 ++-
 2 files changed, 119 insertions(+), 4 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/utils/logs.test.ts 
b/airflow-core/src/airflow/ui/src/utils/logs.test.ts
new file mode 100644
index 00000000000..0308937c28b
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/logs.test.ts
@@ -0,0 +1,108 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { describe, it, expect } from "vitest";
+
+import type { TaskInstancesLogResponse } from "openapi/requests/types.gen";
+
+import { parseStreamingLogContent } from "./logs";
+
+describe("parseStreamingLogContent", () => {
+  it("returns content when data has content property", () => {
+    const data: TaskInstancesLogResponse = {
+      content: ["log line 1", "log line 2"],
+      continuation_token: null,
+    };
+
+    expect(parseStreamingLogContent(data)).toEqual(["log line 1", "log line 
2"]);
+  });
+
+  it("parses string data as newline-separated JSON", () => {
+    const data = '"log line 1"\n"log line 2"\n"log line 3"' as unknown as 
TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual(["log line 1", "log line 
2", "log line 3"]);
+  });
+
+  it("filters out empty lines when parsing string data", () => {
+    const data = '"log line 1"\n\n"log line 2"\n\n\n"log line 3"' as unknown 
as TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual(["log line 1", "log line 
2", "log line 3"]);
+  });
+
+  it("returns empty array for undefined data", () => {
+    expect(parseStreamingLogContent(undefined)).toEqual([]);
+  });
+
+  it("returns empty array for null data", () => {
+    // eslint-disable-next-line unicorn/no-null
+    const data = null as unknown as TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual([]);
+  });
+
+  it("returns object as array when data is an object without content 
property", () => {
+    const data = { someOtherProperty: "value" } as unknown as 
TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual([{ someOtherProperty: 
"value" }]);
+  });
+
+  it("returns empty array for number data", () => {
+    const data = 123 as unknown as TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual([]);
+  });
+
+  it("returns empty array when JSON parsing fails", () => {
+    const data = "invalid json line\nanother invalid line" as unknown as 
TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual([]);
+  });
+
+  it("handles single log line as string", () => {
+    const data = '"single log line"' as unknown as TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual(["single log line"]);
+  });
+
+  it("handles empty string", () => {
+    const data = "" as unknown as TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual([]);
+  });
+
+  it("handles data with empty content array", () => {
+    const data: TaskInstancesLogResponse = {
+      content: [],
+      continuation_token: null,
+    };
+
+    expect(parseStreamingLogContent(data)).toEqual([]);
+  });
+
+  it("handles single log line object", () => {
+    const data = {
+      level: "info",
+      message: "log message",
+      timestamp: "2024-01-01",
+    } as unknown as TaskInstancesLogResponse;
+
+    expect(parseStreamingLogContent(data)).toEqual([
+      { level: "info", message: "log message", timestamp: "2024-01-01" },
+    ]);
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/logs.ts 
b/airflow-core/src/airflow/ui/src/utils/logs.ts
index 4658bc2070b..21f12c660b0 100644
--- a/airflow-core/src/airflow/ui/src/utils/logs.ts
+++ b/airflow-core/src/airflow/ui/src/utils/logs.ts
@@ -57,11 +57,13 @@ export const logLevelOptions = createListCollection<{
 export const parseStreamingLogContent = (
   data: TaskInstancesLogResponse | undefined,
 ): TaskInstancesLogResponse["content"] => {
-  if (!data?.content) {
-    const content = data as unknown as string;
+  if (data?.content) {
+    return data.content;
+  }
 
+  if (typeof data === "string") {
     try {
-      return content
+      return (data as string)
         .split("\n")
         .filter((line) => line.trim() !== "")
         .map((line) => JSON.parse(line) as string);
@@ -70,5 +72,10 @@ export const parseStreamingLogContent = (
     }
   }
 
-  return data.content;
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+  if (typeof data === "object" && data !== null) {
+    return [data] as unknown as TaskInstancesLogResponse["content"];
+  }
+
+  return [];
 };

Reply via email to