fix(gateway): align handshake client timeouts

This commit is contained in:
Peter Steinberger
2026-04-29 05:53:38 +01:00
parent 5e2f6ce294
commit 7994833fac
26 changed files with 432 additions and 33 deletions

View File

@@ -138,6 +138,43 @@ describe("probeGatewayStatus", () => {
});
});
it("forwards configured handshake timeout to the connect probe and status RPC", async () => {
callGatewayMock.mockReset();
probeGatewayMock.mockReset();
callGatewayMock.mockResolvedValueOnce({ status: "ok" });
probeGatewayMock.mockResolvedValueOnce({
ok: true,
auth: {
role: "operator",
scopes: ["operator.admin"],
capability: "admin_capable",
},
});
const config = { gateway: { handshakeTimeoutMs: 30_000 } };
await probeGatewayStatus({
url: "ws://127.0.0.1:19191",
token: "temp-token",
config,
preauthHandshakeTimeoutMs: 30_000,
timeoutMs: 30_000,
requireRpc: true,
});
expect(probeGatewayMock).toHaveBeenCalledWith(
expect.objectContaining({
preauthHandshakeTimeoutMs: 30_000,
timeoutMs: 30_000,
}),
);
expect(callGatewayMock).toHaveBeenCalledWith(
expect.objectContaining({
config,
timeoutMs: 30_000,
}),
);
});
it("falls back to read-only when the status RPC succeeds but the auth probe is inconclusive", async () => {
callGatewayMock.mockReset();
probeGatewayMock.mockReset();

View File

@@ -1,3 +1,4 @@
import type { OpenClawConfig } from "../../config/types.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { withProgress } from "../progress.js";
@@ -27,8 +28,10 @@ export async function probeGatewayStatus(opts: {
url: string;
token?: string;
password?: string;
config?: OpenClawConfig;
tlsFingerprint?: string;
timeoutMs: number;
preauthHandshakeTimeoutMs?: number;
json?: boolean;
requireRpc?: boolean;
configPath?: string;
@@ -50,6 +53,9 @@ export async function probeGatewayStatus(opts: {
password: opts.password,
},
tlsFingerprint: opts.tlsFingerprint,
...(opts.preauthHandshakeTimeoutMs !== undefined
? { preauthHandshakeTimeoutMs: opts.preauthHandshakeTimeoutMs }
: {}),
timeoutMs: opts.timeoutMs,
includeDetails: false,
};
@@ -60,6 +66,7 @@ export async function probeGatewayStatus(opts: {
token: opts.token,
password: opts.password,
tlsFingerprint: opts.tlsFingerprint,
...(opts.config ? { config: opts.config } : {}),
method: "status",
timeoutMs: opts.timeoutMs,
...(opts.configPath ? { configPath: opts.configPath } : {}),

View File

@@ -231,6 +231,31 @@ describe("gatherDaemonStatus", () => {
);
});
it("uses configured handshake timeout as the default daemon probe budget", async () => {
daemonLoadedConfig = {
gateway: {
bind: "lan",
tls: { enabled: true },
handshakeTimeoutMs: 30_000,
auth: { token: "daemon-token" },
},
};
await gatherDaemonStatus({
rpc: {},
probe: true,
deep: false,
});
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
expect.objectContaining({
config: daemonLoadedConfig,
preauthHandshakeTimeoutMs: 30_000,
timeoutMs: 30_000,
}),
);
});
it("reuses the shared CLI config snapshot when the daemon uses the same config path", async () => {
serviceReadCommand.mockResolvedValueOnce({
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],

View File

@@ -478,7 +478,9 @@ export async function gatherDaemonStatus(
.catch(() => [])
: [];
const timeoutMs = parseStrictPositiveInteger(opts.rpc.timeout ?? "10000") ?? 10_000;
const timeoutMs =
parseStrictPositiveInteger(opts.rpc.timeout ?? undefined) ??
Math.max(10_000, daemonCfg.gateway?.handshakeTimeoutMs ?? 0);
const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true;
const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled;
@@ -513,10 +515,12 @@ export async function gatherDaemonStatus(
url: gateway.probeUrl,
token: daemonProbeAuth?.token,
password: daemonProbeAuth?.password,
config: daemonCfg,
tlsFingerprint:
shouldUseLocalTlsRuntime && tlsRuntime?.enabled
? tlsRuntime.fingerprintSha256
: undefined,
preauthHandshakeTimeoutMs: daemonCfg.gateway?.handshakeTimeoutMs,
timeoutMs,
json: opts.rpc.json,
requireRpc: opts.requireRpc,