mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(gateway): bound e2e HTTP helper responses
This commit is contained in:
@@ -15,7 +15,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway: keep dev smoke scripts on the current protocol version and make the kitchen-sink RPC walk fail on dropped diagnostics or aggregate Gateway RSS spikes.
|
||||
- Gateway: make the CPU scenario checker fail when completed Gateway runs report hot CPU observations instead of only writing them to artifacts.
|
||||
- CLI: bound startup-memory probes so a hung startup command fails with timeout guidance instead of hanging the memory gate indefinitely.
|
||||
|
||||
## 2026.5.26
|
||||
|
||||
### Highlights
|
||||
|
||||
73
test/helpers/gateway-e2e-harness.test.ts
Normal file
73
test/helpers/gateway-e2e-harness.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { postJson } from "./gateway-e2e-harness.js";
|
||||
|
||||
let server: Server | undefined;
|
||||
|
||||
afterEach(async () => {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server?.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
server = undefined;
|
||||
});
|
||||
|
||||
async function listen(handler: Parameters<typeof createServer>[0]): Promise<string> {
|
||||
server = createServer(handler);
|
||||
await new Promise<void>((resolve) => {
|
||||
server?.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("test server did not get a TCP address");
|
||||
}
|
||||
return `http://127.0.0.1:${address.port}`;
|
||||
}
|
||||
|
||||
describe("postJson", () => {
|
||||
it("times out stalled Gateway HTTP helpers", async () => {
|
||||
const baseUrl = await listen((_req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.write('{"ok":');
|
||||
});
|
||||
|
||||
await expect(postJson(`${baseUrl}/stall`, {}, undefined, { timeoutMs: 25 })).rejects.toThrow(
|
||||
"timed out after 25ms",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a wall-clock timeout instead of an idle socket timeout", async () => {
|
||||
const baseUrl = await listen((_req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
const interval = setInterval(() => {
|
||||
res.write(" ");
|
||||
}, 5);
|
||||
res.on("close", () => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
});
|
||||
|
||||
await expect(postJson(`${baseUrl}/slow`, {}, undefined, { timeoutMs: 30 })).rejects.toThrow(
|
||||
"timed out after 30ms",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects oversized Gateway HTTP helper responses", async () => {
|
||||
const baseUrl = await listen((_req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ data: "x".repeat(128) }));
|
||||
});
|
||||
|
||||
await expect(
|
||||
postJson(`${baseUrl}/large`, {}, undefined, { maxResponseBytes: 32 }),
|
||||
).rejects.toThrow("response exceeded 32 bytes");
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,13 @@ export type GatewayInstance = OpenClawTestInstance;
|
||||
const GATEWAY_CONNECT_STATUS_TIMEOUT_MS = 10_000;
|
||||
const GATEWAY_NODE_STATUS_TIMEOUT_MS = 15_000;
|
||||
const GATEWAY_NODE_STATUS_POLL_MS = 20;
|
||||
const POST_JSON_TIMEOUT_MS = 15_000;
|
||||
const POST_JSON_MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||
|
||||
export type PostJsonOptions = {
|
||||
maxResponseBytes?: number;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export async function spawnGatewayInstance(name: string): Promise<GatewayInstance> {
|
||||
const inst = await createOpenClawTestInstance({ name });
|
||||
@@ -43,10 +50,32 @@ export async function postJson(
|
||||
url: string,
|
||||
body: unknown,
|
||||
headers?: Record<string, string>,
|
||||
options: PostJsonOptions = {},
|
||||
): Promise<{ status: number; json: unknown }> {
|
||||
const payload = JSON.stringify(body);
|
||||
const parsed = new URL(url);
|
||||
const timeoutMs = options.timeoutMs ?? POST_JSON_TIMEOUT_MS;
|
||||
const maxResponseBytes = options.maxResponseBytes ?? POST_JSON_MAX_RESPONSE_BYTES;
|
||||
return await new Promise<{ status: number; json: unknown }>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let responseBytes = 0;
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
const finish = (result: { status: number; json: unknown } | { error: Error }) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
if ("error" in result) {
|
||||
reject(result.error);
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const req = httpRequest(
|
||||
{
|
||||
method: "POST",
|
||||
@@ -63,6 +92,14 @@ export async function postJson(
|
||||
let data = "";
|
||||
res.setEncoding("utf8");
|
||||
res.on("data", (chunk) => {
|
||||
responseBytes += Buffer.byteLength(chunk, "utf8");
|
||||
if (responseBytes > maxResponseBytes) {
|
||||
const error = new Error(`POST ${url} response exceeded ${maxResponseBytes} bytes`);
|
||||
req.destroy(error);
|
||||
res.destroy(error);
|
||||
finish({ error });
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
@@ -74,11 +111,16 @@ export async function postJson(
|
||||
json = data;
|
||||
}
|
||||
}
|
||||
resolve({ status: res.statusCode ?? 0, json });
|
||||
finish({ status: res.statusCode ?? 0, json });
|
||||
});
|
||||
res.on("error", (error) => finish({ error }));
|
||||
},
|
||||
);
|
||||
req.on("error", reject);
|
||||
timeout = setTimeout(() => {
|
||||
req.destroy(new Error(`POST ${url} timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
timeout.unref?.();
|
||||
req.on("error", (error) => finish({ error }));
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user