fix(gateway): increase WS handshake timeout from 3s to 10s (#49262)

* fix(gateway): increase WS handshake timeout from 3s to 10s

The 3-second default is too aggressive when the event loop is under load
(concurrent sessions, compaction, agent turns), causing spurious
'gateway closed (1000)' errors on CLI commands like `openclaw cron list`.

Changes:
- Increase DEFAULT_HANDSHAKE_TIMEOUT_MS from 3_000 to 10_000
- Add OPENCLAW_HANDSHAKE_TIMEOUT_MS env var for user override (no VITEST gate)
- Keep OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS as fallback for existing tests

Fixes #46892

* fix: restore VITEST guard on test env var, use || for empty-string fallback, fix formatting

* fix: cover gateway handshake timeout env override (#49262) (thanks @fuller-stack-dev)

---------

Co-authored-by: Wilfred <wilfred@Wilfreds-Mac-mini.local>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
fuller-stack-dev
2026-03-19 11:16:40 -06:00
committed by GitHub
parent 3dfd8eef7f
commit 36f394c299
3 changed files with 31 additions and 3 deletions

View File

@@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai
- Slack/startup: harden `@slack/bolt` import interop across current bundled runtime shapes so Slack monitors no longer crash with `App is not a constructor` after plugin-sdk bundling changes. (#45953) Thanks @merc1305.
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup. (#47601) Thanks @ngutman.
- Gateway/WS handshake: raise the default pre-auth handshake timeout to 10 seconds and add `OPENCLAW_HANDSHAKE_TIMEOUT_MS` as a runtime override so busy local gateways stop dropping healthy CLI connections at 3 seconds. (#49262) Thanks @fuller-stack-dev.
- Gateway/websocket pairing bypass for disabled auth: skip device-pairing enforcement for Control UI operator sessions when `gateway.auth.mode=none`, so reverse-proxied dashboards no longer get stuck on `pairing required` despite auth being explicitly disabled. (#47148) Thanks @ademczuk.
- Control UI/model switching: preserve the selected provider prefix when switching models from the chat dropdown, so multi-provider setups no longer send `anthropic/gpt-5.2`-style mismatches when the user picked `openai/gpt-5.2`. (#47581) Thanks @chrishham.
- Control UI/storage: scope persisted settings keys by gateway base path, with migration from the legacy shared key, so multiple gateways under one domain stop overwriting each other's dashboard preferences. (#47932) Thanks @bobBot-claw.

View File

@@ -21,10 +21,14 @@ export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => {
maxChatHistoryMessagesBytes = value;
}
};
export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 3_000;
export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
export const getHandshakeTimeoutMs = () => {
if (process.env.VITEST && process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS) {
const parsed = Number(process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS);
// User-facing env var (works in all environments); test-only var gated behind VITEST
const envKey =
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS ||
(process.env.VITEST && process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS);
if (envKey) {
const parsed = Number(envKey);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}

View File

@@ -93,6 +93,29 @@ export function registerDefaultAuthTokenSuite(): void {
}
});
test("prefers OPENCLAW_HANDSHAKE_TIMEOUT_MS and falls back on empty string", () => {
const prevHandshakeTimeout = process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS;
const prevTestHandshakeTimeout = process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS;
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS = "75";
process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS = "20";
try {
expect(getHandshakeTimeoutMs()).toBe(75);
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS = "";
expect(getHandshakeTimeoutMs()).toBe(20);
} finally {
if (prevHandshakeTimeout === undefined) {
delete process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS;
} else {
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS = prevHandshakeTimeout;
}
if (prevTestHandshakeTimeout === undefined) {
delete process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS;
} else {
process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS = prevTestHandshakeTimeout;
}
}
});
test("connect (req) handshake returns hello-ok payload", async () => {
const { STATE_DIR, createConfigIO } = await import("../config/config.js");
const ws = await openWs(port);