mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(clickclack): bound REST success JSON response reads (#96970)
* fix(clickclack): bound REST success JSON response reads * test(clickclack): harden response cap proof --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
co-authored by
Vincent Koc
parent
2100ee7cc8
commit
f4fa10c2c5
@@ -1,6 +1,81 @@
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createClickClackClient } from "./http-client.js";
|
||||
|
||||
const LOOPBACK_RESPONSE_BYTES = 18 * 1024 * 1024;
|
||||
|
||||
async function listenLoopbackServer(server: Server): Promise<number> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
reject(new Error("expected loopback TCP address"));
|
||||
return;
|
||||
}
|
||||
resolve(address.port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createOversizedJsonServer(): { server: Server; closed: Promise<number> } {
|
||||
let resolveClosed: (sentBytes: number) => void = () => {};
|
||||
const closed = new Promise<number>((resolve) => {
|
||||
resolveClosed = resolve;
|
||||
});
|
||||
const server = createServer((req, res) => {
|
||||
let sentBytes = 0;
|
||||
let stopped = false;
|
||||
let prefixSent = false;
|
||||
const prefixChunk = Buffer.from('{"user":{"id":"');
|
||||
const bodyChunk = Buffer.alloc(64 * 1024, 0x61);
|
||||
const suffixChunk = Buffer.from('"}}');
|
||||
const writeBuffer = (buffer: Buffer) => {
|
||||
sentBytes += buffer.length;
|
||||
if (!res.write(buffer)) {
|
||||
res.once("drain", writeChunks);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const writeChunks = () => {
|
||||
if (!prefixSent) {
|
||||
prefixSent = true;
|
||||
if (!writeBuffer(prefixChunk)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
if (sentBytes + bodyChunk.length + suffixChunk.length >= LOOPBACK_RESPONSE_BYTES) {
|
||||
break;
|
||||
}
|
||||
if (!writeBuffer(bodyChunk)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!stopped) {
|
||||
sentBytes += suffixChunk.length;
|
||||
res.end(suffixChunk);
|
||||
}
|
||||
};
|
||||
res.writeHead(200, { connection: "close", "content-type": "application/json" });
|
||||
res.on("close", () => {
|
||||
stopped = true;
|
||||
resolveClosed(sentBytes);
|
||||
});
|
||||
req.on("aborted", () => {
|
||||
stopped = true;
|
||||
res.destroy();
|
||||
});
|
||||
writeChunks();
|
||||
});
|
||||
return { server, closed };
|
||||
}
|
||||
|
||||
function streamedErrorResponse(body: string, limit: number) {
|
||||
const encoded = new TextEncoder().encode(body);
|
||||
let readCount = 0;
|
||||
@@ -39,6 +114,25 @@ function streamedErrorResponse(body: string, limit: number) {
|
||||
}
|
||||
|
||||
describe("ClickClack HTTP client", () => {
|
||||
it("bounds oversized success JSON responses and closes the stream early", async () => {
|
||||
const { server, closed } = createOversizedJsonServer();
|
||||
const port = await listenLoopbackServer(server);
|
||||
const client = createClickClackClient({
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
token: "test-token",
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(client.me()).rejects.toThrow(
|
||||
"ClickClack response: JSON response exceeds 16777216 bytes",
|
||||
);
|
||||
const sentBytes = await closed;
|
||||
expect(sentBytes).toBeLessThan(LOOPBACK_RESPONSE_BYTES);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds error response bodies without using raw response.text()", async () => {
|
||||
const streamed = streamedErrorResponse("x".repeat(9000), 8 * 1024);
|
||||
const fetchMock = vi.fn(async () => streamed.response);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* Thin ClickClack REST/websocket client used by gateway, resolver, and outbound
|
||||
* delivery code.
|
||||
*/
|
||||
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { WebSocket } from "ws";
|
||||
import type {
|
||||
ClickClackChannel,
|
||||
@@ -44,7 +47,7 @@ export function createClickClackClient(options: ClientOptions) {
|
||||
const detail = await readResponseTextLimited(response, CLICKCLACK_ERROR_BODY_LIMIT_BYTES);
|
||||
throw new Error(`ClickClack ${response.status}: ${detail}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
return await readProviderJsonResponse<T>(response, "ClickClack response");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user