From e5db650a488e78a83279bdea340dec54c8112ebe 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 (cherry picked from commit 3eec404aab29e9dd78d2e69c9b4e2985f6a326fe) --- 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 d46439a61f4b..ad1a63e9e480 100644 --- a/extensions/clickclack/src/http-client.ts +++ b/extensions/clickclack/src/http-client.ts @@ -22,6 +22,10 @@ type ClientOptions = { }; const CLICKCLACK_ERROR_BODY_LIMIT_BYTES = 8 * 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; /** * Creates a typed client for the ClickClack API using bearer-token auth. @@ -146,6 +150,7 @@ export function createClickClackClient(options: ClientOptions) { headers: { Authorization: `Bearer ${options.token}`, }, + handshakeTimeout: CLICKCLACK_WEBSOCKET_HANDSHAKE_TIMEOUT_MS, }); }, }; 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, + }, + }, + ]); + }); +});