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 [];
};