fix(signal): bound GitHub release info JSON response with readProviderJsonResponse (#97536)

Replace bare `await response.json()` in `installSignalCliFromRelease` with
`readProviderJsonResponse` (16 MiB cap, stream cancel on overflow). The
external GitHub Releases endpoint can include a large `body` changelog field;
the error path was already guarded but the success path was unbounded.
The existing inner catch continues to convert overflow errors into the
graceful `{ ok: false, error: "Failed to parse signal-cli release info." }` path.

Adds a regression test verifying the stream is cancelled before all chunks are
read on an oversized 20 MiB streaming response.

Co-authored-by: NIO <nocodet@mail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 51064bda4d)
This commit is contained in:
NIO
2026-06-29 02:46:25 +08:00
committed by Dallin Romney
parent 9e337073c9
commit 9cba1e77cb
2 changed files with 34 additions and 1 deletions
@@ -277,6 +277,38 @@ describe("installSignalCliFromRelease", () => {
expect(fetchResult.release).toHaveBeenCalledTimes(1);
});
it("bounds oversized GitHub release metadata and cancels the stream", async () => {
const chunkSize = 1024 * 1024;
const chunkCount = 20; // 20 MiB — over the 16 MiB cap
let readCount = 0;
let canceled = false;
const oversized = new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
if (readCount >= chunkCount) {
controller.close();
return;
}
readCount += 1;
controller.enqueue(new Uint8Array(chunkSize));
},
cancel() {
canceled = true;
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
const releaseMock = vi.fn().mockResolvedValue(undefined);
fetchWithSsrFGuardMock.mockResolvedValue({ response: oversized, release: releaseMock });
const result = await installSignalCliFromRelease({ log: vi.fn() } as unknown as RuntimeEnv);
expect(result).toEqual({ ok: false, error: "Failed to parse signal-cli release info." });
expect(canceled).toBe(true);
expect(readCount).toBeLessThan(chunkCount);
expect(releaseMock).toHaveBeenCalledTimes(1);
});
it("bounds the release metadata request with an explicit timeout", async () => {
const fetchResult = okDownloadResponse(JSON.stringify({ tag_name: "v0.14.3", assets: [] }), {
headers: { "content-type": "application/json" },
+2 -1
View File
@@ -5,6 +5,7 @@ import path from "node:path";
import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { runPluginCommandWithTimeout } from "openclaw/plugin-sdk/run-command";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { CONFIG_DIR, extractArchive, resolveBrewExecutable } from "openclaw/plugin-sdk/setup-tools";
@@ -303,7 +304,7 @@ export async function installSignalCliFromRelease(
};
}
try {
payload = (await response.json()) as ReleaseResponse;
payload = await readProviderJsonResponse<ReleaseResponse>(response, "signal.release-info");
} catch {
return {
ok: false,