From 3eec404aab29e9dd78d2e69c9b4e2985f6a326fe Mon Sep 17 00:00:00 2001 From: NIO Date: Thu, 16 Jul 2026 16:41:53 +0800 Subject: [PATCH] fix(clickclack): bound websocket handshake waits at 30s (#106485) * fix(clickclack): bound websocket handshake waits at 30s * test(clickclack): prove WebSocket handshake deadline --------- Co-authored-by: Peter Steinberger --- extensions/clickclack/src/http-client.ts | 5 +++ .../src/http-client.websocket-options.test.ts | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 extensions/clickclack/src/http-client.websocket-options.test.ts diff --git a/extensions/clickclack/src/http-client.ts b/extensions/clickclack/src/http-client.ts index 2ce3d204d086..66c533c085c7 100644 --- a/extensions/clickclack/src/http-client.ts +++ b/extensions/clickclack/src/http-client.ts @@ -65,6 +65,10 @@ const CLICKCLACK_CORRELATION_ID_HEADER = "X-Correlation-ID"; // accepts 1 MiB request bodies, then wraps and re-encodes them as events, so a // valid frame can exceed 1 MiB before ws hands it to the event parser. const CLICKCLACK_INBOUND_JSON_LIMIT_BYTES = 16 * 1024 * 1024; +// Match Slack relay / Mattermost / Signal channel gateway handshake floors. +// Without this, gateway.ts waits forever for close/error when TCP accepts but +// never upgrades, pinning the monitor reconnect loop. +const CLICKCLACK_WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 30_000; class ClickClackHttpError extends Error { constructor( @@ -410,6 +414,7 @@ export function createClickClackClient(options: ClientOptions) { headers: { Authorization: `Bearer ${options.token}`, }, + handshakeTimeout: CLICKCLACK_WEBSOCKET_HANDSHAKE_TIMEOUT_MS, maxPayload: CLICKCLACK_INBOUND_JSON_LIMIT_BYTES, }); }, diff --git a/extensions/clickclack/src/http-client.websocket-options.test.ts b/extensions/clickclack/src/http-client.websocket-options.test.ts new file mode 100644 index 000000000000..8b3ad55a9d6a --- /dev/null +++ b/extensions/clickclack/src/http-client.websocket-options.test.ts @@ -0,0 +1,40 @@ +// ClickClack tests cover websocket constructor options. +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { webSocketCtorCalls } = vi.hoisted(() => ({ + webSocketCtorCalls: [] as Array<{ url: string; options: unknown }>, +})); + +vi.mock("ws", () => ({ + WebSocket: function MockWebSocket(url: string | URL, options?: unknown) { + webSocketCtorCalls.push({ url: String(url), options }); + }, +})); + +import { createClickClackClient } from "./http-client.js"; + +describe("createClickClackClient websocket options", () => { + beforeEach(() => { + webSocketCtorCalls.length = 0; + }); + + it("passes a 30-second opening handshake deadline to ws", () => { + const client = createClickClackClient({ + baseUrl: "https://clickclack.example", + token: "fake", + }); + + client.websocket("workspace-1", "cursor-1"); + + expect(webSocketCtorCalls).toEqual([ + { + url: "wss://clickclack.example/api/realtime/ws?workspace_id=workspace-1&after_cursor=cursor-1", + options: { + headers: { Authorization: "Bearer fake" }, + handshakeTimeout: 30_000, + maxPayload: 16 * 1024 * 1024, + }, + }, + ]); + }); +});