fix(gateway): harden local reachability checks

Co-authored-by: arthurianresolve <arthurianresolve@users.noreply.github.com>
Co-authored-by: codexGW <9350182+codexGW@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-28 20:56:46 +01:00
parent 3d736f67cf
commit 0a2d635e68
8 changed files with 290 additions and 12 deletions

View File

@@ -25,6 +25,7 @@ const DEFAULT_DISCORD_GATEWAY_INFO_TIMEOUT_MS = 30_000;
const MAX_DISCORD_GATEWAY_INFO_TIMEOUT_MS = 120_000;
const DISCORD_GATEWAY_INFO_TIMEOUT_ENV = "OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS";
const DISCORD_GATEWAY_METADATA_FALLBACK_LOG_INTERVAL_MS = 60_000;
const DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS = 30_000;
type DiscordGatewayMetadataResponse = Pick<Response, "ok" | "status" | "text">;
type DiscordGatewayFetchInit = Record<string, unknown> & {
@@ -36,7 +37,10 @@ type DiscordGatewayFetch = (
) => Promise<DiscordGatewayMetadataResponse>;
type DiscordGatewayMetadataError = Error & { transient?: boolean };
type DiscordGatewayWebSocketCtor = new (url: string, options?: { agent?: unknown }) => ws.WebSocket;
type DiscordGatewayWebSocketCtor = new (
url: string,
options?: { agent?: unknown; handshakeTimeout?: number },
) => ws.WebSocket;
const registrationPromises = new WeakMap<carbonGateway.GatewayPlugin, Promise<void>>();
const gatewayMetadataFallbackLogLastAt = new WeakMap<RuntimeEnv, number>();
type CarbonGatewayRegistrationState = {
@@ -421,7 +425,10 @@ function createGatewayPlugin(params: {
// close-path crashes during Discord gateway teardown; the ws transport is
// already our proxy path and behaves predictably for lifecycle cleanup.
const WebSocketCtor = params.testing?.webSocketCtor ?? ws.default;
const socket = new WebSocketCtor(url, params.wsAgent ? { agent: params.wsAgent } : undefined);
const socket = new WebSocketCtor(url, {
handshakeTimeout: DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS,
...(params.wsAgent ? { agent: params.wsAgent } : {}),
});
const emitTransportActivity = () => {
if ((this as unknown as { ws?: unknown }).ws !== socket) {
return;

View File

@@ -108,7 +108,10 @@ vi.mock("https-proxy-agent", () => ({
}));
vi.mock("ws", () => ({
default: function MockWebSocket(url: string, options?: { agent?: unknown }) {
default: function MockWebSocket(
url: string,
options?: { agent?: unknown; handshakeTimeout?: number },
) {
webSocketSpy(url, options);
},
}));
@@ -159,9 +162,15 @@ describe("createDiscordGatewayPlugin", () => {
return {
HttpsProxyAgentCtor:
HttpsProxyAgent as unknown as typeof import("https-proxy-agent").HttpsProxyAgent,
webSocketCtor: function WebSocketCtor(url: string, options?: { agent?: unknown }) {
webSocketCtor: function WebSocketCtor(
url: string,
options?: { agent?: unknown; handshakeTimeout?: number },
) {
webSocketSpy(url, options);
} as unknown as new (url: string, options?: { agent?: unknown }) => import("ws").WebSocket,
} as unknown as new (
url: string,
options?: { agent?: unknown; handshakeTimeout?: number },
) => import("ws").WebSocket,
registerClient: async (_plugin: unknown, client: unknown) => {
baseRegisterClientSpy(client);
},
@@ -295,7 +304,9 @@ describe("createDiscordGatewayPlugin", () => {
.createWebSocket;
createWebSocket("wss://gateway.discord.gg");
expect(webSocketSpy).toHaveBeenCalledWith("wss://gateway.discord.gg", undefined);
expect(webSocketSpy).toHaveBeenCalledWith("wss://gateway.discord.gg", {
handshakeTimeout: 30_000,
});
expect(wsProxyAgentSpy).not.toHaveBeenCalled();
});
@@ -409,7 +420,7 @@ describe("createDiscordGatewayPlugin", () => {
expect(wsProxyAgentSpy).toHaveBeenCalledWith("http://127.0.0.1:8080");
expect(webSocketSpy).toHaveBeenCalledWith(
"wss://gateway.discord.gg",
expect.objectContaining({ agent: getLastAgent() }),
expect.objectContaining({ agent: getLastAgent(), handshakeTimeout: 30_000 }),
);
expect(runtime.log).toHaveBeenCalledWith("discord: gateway proxy enabled");
expect(runtime.error).not.toHaveBeenCalled();