fix(vscode): avoid render crash on malformed api_req payloads in combineApiRequests (#13560)

This commit is contained in:
𝓜𝓲𝓼𝓼𝓪𝓻𝓲 𝓐𝓱𝓲𝓵 🌿
2026-08-26 19:49:59 +05:30
committed by GitHub
parent c0c37a1587
commit 7673b30e4d
2 changed files with 85 additions and 2 deletions
@@ -0,0 +1,66 @@
import { describe, it } from "bun:test"
import { strict as assert } from "node:assert"
import type { ClineMessage } from "./ExtensionMessage"
import { combineApiRequests } from "./combineApiRequests"
function started(ts: number, text: string) {
return { ts, type: "say", say: "api_req_started", text } as ClineMessage
}
function finished(ts: number, text: string) {
return { ts, type: "say", say: "api_req_finished", text } as ClineMessage
}
describe("combineApiRequests", () => {
it("merges a started request with its following finished payload", () => {
const messages: ClineMessage[] = [
started(1, JSON.stringify({ request: "GET /api/data" })),
finished(2, JSON.stringify({ cost: 0.005, tokensIn: 10, tokensOut: 5 })),
]
const combined = combineApiRequests(messages)
assert.equal(combined.length, 1)
assert.equal(combined[0]?.say, "api_req_started")
assert.deepEqual(JSON.parse(combined[0]?.text ?? "{}"), {
request: "GET /api/data",
cost: 0.005,
tokensIn: 10,
tokensOut: 5,
})
})
it("keeps an api_req_started without a matching finished payload", () => {
const messages: ClineMessage[] = [started(1, JSON.stringify({ request: "A" }))]
const combined = combineApiRequests(messages)
assert.equal(combined.length, 1)
assert.deepEqual(JSON.parse(combined[0]?.text ?? "{}"), { request: "A" })
})
it("does not throw on a malformed api_req_started payload and keeps the row", () => {
const messages: ClineMessage[] = [
started(1, "{not-json"),
{ ts: 2, type: "say", say: "text", text: "hello" } as ClineMessage,
]
const combined = combineApiRequests(messages)
assert.equal(combined[0]?.text, "{not-json")
assert.equal(combined[1]?.text, "hello")
})
it("does not throw on a malformed api_req_finished payload", () => {
const messages: ClineMessage[] = [
started(1, JSON.stringify({ request: "A", cost: 0.1 })),
finished(2, "{not-json"),
]
const combined = combineApiRequests(messages)
// Pair is still consumed; the started row survives with its own payload.
assert.equal(combined.length, 1)
assert.deepEqual(JSON.parse(combined[0]?.text ?? "{}"), { request: "A", cost: 0.1 })
})
})
+19 -2
View File
@@ -23,12 +23,29 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] {
for (let i = 0; i < messages.length; i++) {
if (messages[i].type === "say" && messages[i].say === "api_req_started") {
const startedRequest = JSON.parse(messages[i].text || "{}")
// Malformed payloads are tolerated everywhere else that reads this row
// (see messageUtils, RequestStartRow, getApiMetrics) because this runs
// inside the render pipeline. A JSON.parse throw here would crash the
// whole chat view, so degrade to keeping the original row instead.
let startedRequest: Record<string, unknown>
try {
startedRequest = JSON.parse(messages[i].text || "{}")
} catch {
continue
}
let j = i + 1
while (j < messages.length) {
if (messages[j].type === "say" && messages[j].say === "api_req_finished") {
const finishedRequest = JSON.parse(messages[j].text || "{}")
let finishedRequest: Record<string, unknown>
try {
finishedRequest = JSON.parse(messages[j].text || "{}")
} catch {
// Unparseable finish payload — nothing to merge, keep the
// started row as-is but still consume the pair.
finishedRequest = {}
}
const combinedRequest = {
...startedRequest,
...finishedRequest,