fix(signal): cap client request timeouts

This commit is contained in:
Peter Steinberger
2026-05-29 16:56:55 -04:00
parent 31169ff3b4
commit e6b011823e
2 changed files with 28 additions and 5 deletions

View File

@@ -17,6 +17,8 @@ let signalCheck: typeof import("./client.js").signalCheck;
let signalRpcRequest: typeof import("./client.js").signalRpcRequest;
let streamSignalEvents: typeof import("./client.js").streamSignalEvents;
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
const servers: http.Server[] = [];
async function readRequestBody(req: IncomingMessage): Promise<string> {
@@ -51,6 +53,7 @@ beforeAll(async () => {
});
afterEach(async () => {
vi.restoreAllMocks();
await Promise.all(
servers.splice(0).map(
(server) =>
@@ -221,6 +224,24 @@ describe("signalRpcRequest", () => {
}),
).rejects.toThrow("Signal HTTP exceeded deadline after 25ms");
});
it("caps oversized RPC request timeouts before scheduling", async () => {
const timeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
const baseUrl = await withSignalServer((_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ jsonrpc: "2.0", result: { version: "0.13.22" }, id: "test-id" }));
});
await signalRpcRequest("version", undefined, {
baseUrl,
timeoutMs: MAX_TIMER_TIMEOUT_MS + 1_000_000,
});
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
});
describe("signalCheck", () => {

View File

@@ -3,6 +3,7 @@ import http, { type ClientRequest, type IncomingMessage } from "node:http";
import https from "node:https";
import { generateSecureUuid } from "openclaw/plugin-sdk/core";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
export type SignalRpcOptions = {
baseUrl: string;
@@ -106,7 +107,7 @@ function normalizeSignalSseTimeoutMs(timeoutMs: number): number | null {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
return null;
}
return timeoutMs;
return resolveTimerTimeoutMs(timeoutMs, DEFAULT_TIMEOUT_MS);
}
function requestSignalHttpText(
@@ -120,13 +121,14 @@ function requestSignalHttpText(
},
): Promise<SignalHttpResponse> {
assertSignalHttpProtocol(url, "HTTP");
const timeoutMs = resolveTimerTimeoutMs(options.timeoutMs, DEFAULT_TIMEOUT_MS);
const client = url.protocol === "https:" ? https : http;
return new Promise((resolve, reject) => {
let settled = false;
let request: ClientRequest | undefined;
const deadline = setTimeout(() => {
request?.destroy(new Error(`Signal HTTP exceeded deadline after ${options.timeoutMs}ms`));
}, options.timeoutMs);
request?.destroy(new Error(`Signal HTTP exceeded deadline after ${timeoutMs}ms`));
}, timeoutMs);
deadline.unref?.();
const cleanup = () => {
clearTimeout(deadline);
@@ -180,8 +182,8 @@ function requestSignalHttpText(
});
},
);
request.setTimeout(options.timeoutMs, () => {
request?.destroy(new Error(`Signal HTTP timed out after ${options.timeoutMs}ms`));
request.setTimeout(timeoutMs, () => {
request?.destroy(new Error(`Signal HTTP timed out after ${timeoutMs}ms`));
});
request.on("error", rejectOnce);
if (options.body !== undefined) {