mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:10:51 +00:00
fix(cli): wait for gateway client teardown before exit (#70691)
Verified: - pnpm test src/gateway/call.test.ts Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/Gateway: wait for one-shot gateway RPC clients to finish WebSocket teardown before the CLI process exits, reducing hangs where commands like `openclaw status` or `openclaw version` could finish their work but stay alive until an external timeout killed them.
|
||||
- Thinking defaults/status: raise the implicit default thinking level for reasoning-capable models from legacy `off`/`low` fallback behavior to a safe provider-supported `medium` equivalent when no explicit config default is set, preserve configured-model reasoning metadata when runtime catalog loading is empty, and make `/status` report the same resolved default as runtime.
|
||||
- Gateway/model pricing: fetch OpenRouter and LiteLLM pricing asynchronously at startup and extend catalog fetch timeouts to 30 seconds, reducing noisy timeout warnings during slow upstream responses.
|
||||
- Agents/failover: classify bare undici transport failures (`terminated`, `UND_ERR_SOCKET`, `UND_ERR_CONNECT_TIMEOUT`, body/header timeouts, aborted streams) and pi-ai's openai-codex `Request failed` sentinel as `timeout`, so Cloudflare 502s with empty bodies and mid-response socket resets actually enter the configured fallback chain instead of surfacing as unclassified errors. Fixes #69368. (#69677) Thanks @sk7n4k3d.
|
||||
|
||||
@@ -124,6 +124,7 @@ class StubGatewayClient {
|
||||
}
|
||||
}
|
||||
stop() {}
|
||||
async stopAndWait() {}
|
||||
}
|
||||
|
||||
function resetGatewayCallMocks() {
|
||||
@@ -812,6 +813,68 @@ describe("callGateway error details", () => {
|
||||
expect(lastRequestOptions?.opts?.timeoutMs).toBeUndefined();
|
||||
});
|
||||
|
||||
it("waits for gateway client teardown before resolving", async () => {
|
||||
setLocalLoopbackGatewayConfig();
|
||||
|
||||
let releaseStop!: () => void;
|
||||
let stopStarted = false;
|
||||
let stopFinished = false;
|
||||
let callResolved = false;
|
||||
|
||||
__testing.setDepsForTests({
|
||||
createGatewayClient: (opts) =>
|
||||
({
|
||||
async request(
|
||||
method: string,
|
||||
params: unknown,
|
||||
requestOpts?: { expectFinal?: boolean; timeoutMs?: number | null },
|
||||
) {
|
||||
lastRequestOptions = { method, params, opts: requestOpts };
|
||||
return { ok: true };
|
||||
},
|
||||
start() {
|
||||
opts.onHelloOk?.({
|
||||
features: {
|
||||
methods: helloMethods ?? [],
|
||||
events: [],
|
||||
},
|
||||
} as unknown as Parameters<NonNullable<typeof opts.onHelloOk>>[0]);
|
||||
},
|
||||
stop() {},
|
||||
async stopAndWait() {
|
||||
stopStarted = true;
|
||||
await new Promise<void>((resolve) => {
|
||||
releaseStop = () => {
|
||||
stopFinished = true;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
},
|
||||
}) as never,
|
||||
loadConfig: loadConfig as unknown as () => OpenClawConfig,
|
||||
loadOrCreateDeviceIdentity: () => deviceIdentityState.value,
|
||||
resolveGatewayPort: resolveGatewayPort as unknown as (
|
||||
cfg?: OpenClawConfig,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
) => number,
|
||||
});
|
||||
|
||||
const promise = callGateway({ method: "health" }).then(() => {
|
||||
callResolved = true;
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(stopStarted).toBe(true);
|
||||
});
|
||||
expect(callResolved).toBe(false);
|
||||
|
||||
releaseStop();
|
||||
await promise;
|
||||
|
||||
expect(stopFinished).toBe(true);
|
||||
expect(callResolved).toBe(true);
|
||||
});
|
||||
|
||||
it("fails fast when remote mode is missing remote url", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: { mode: "remote", bind: "loopback", remote: {} },
|
||||
|
||||
@@ -92,6 +92,14 @@ const gatewayCallDeps = {
|
||||
...defaultGatewayCallDeps,
|
||||
};
|
||||
|
||||
async function stopGatewayClient(client: GatewayClient): Promise<void> {
|
||||
try {
|
||||
await client.stopAndWait({ timeoutMs: 1_000 });
|
||||
} catch {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGatewayClientDisplayName(opts: CallGatewayBaseOptions): string | undefined {
|
||||
if (opts.clientDisplayName) {
|
||||
return opts.clientDisplayName;
|
||||
@@ -520,11 +528,11 @@ async function executeGatewayRequestWithScopes<T>(params: {
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
ignoreClose = true;
|
||||
await stopGatewayClient(client);
|
||||
stop(undefined, result);
|
||||
client.stop();
|
||||
} catch (err) {
|
||||
ignoreClose = true;
|
||||
client.stop();
|
||||
await stopGatewayClient(client);
|
||||
stop(err as Error);
|
||||
}
|
||||
},
|
||||
@@ -533,15 +541,17 @@ async function executeGatewayRequestWithScopes<T>(params: {
|
||||
return;
|
||||
}
|
||||
ignoreClose = true;
|
||||
client.stop();
|
||||
stop(new Error(formatGatewayCloseError(code, reason, params.connectionDetails)));
|
||||
void stopGatewayClient(client).finally(() => {
|
||||
stop(new Error(formatGatewayCloseError(code, reason, params.connectionDetails)));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
ignoreClose = true;
|
||||
client.stop();
|
||||
stop(new Error(formatGatewayTimeoutError(timeoutMs, params.connectionDetails)));
|
||||
void stopGatewayClient(client).finally(() => {
|
||||
stop(new Error(formatGatewayTimeoutError(timeoutMs, params.connectionDetails)));
|
||||
});
|
||||
}, safeTimerTimeoutMs);
|
||||
|
||||
client.start();
|
||||
|
||||
Reference in New Issue
Block a user