Skip to content

Commit 94b1427

Browse files
committed
fix(discord): log gateway websocket close details
1 parent f83886c commit 94b1427

2 files changed

Lines changed: 171 additions & 7 deletions

File tree

extensions/discord/src/monitor/gateway-plugin.test.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ vi.mock("openclaw/plugin-sdk/proxy-capture", () => ({
6666

6767
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
6868
danger: (value: string) => value,
69+
warn: (value: string) => value,
6970
}));
7071

7172
describe("createDiscordGatewayPlugin", () => {
@@ -86,14 +87,15 @@ describe("createDiscordGatewayPlugin", () => {
8687
function createPlugin(
8788
testing?: NonNullable<Parameters<typeof createDiscordGatewayPlugin>[0]["testing"]>,
8889
discordConfig: Parameters<typeof createDiscordGatewayPlugin>[0]["discordConfig"] = {},
90+
runtime: Parameters<typeof createDiscordGatewayPlugin>[0]["runtime"] = {
91+
log: vi.fn(),
92+
error: vi.fn(),
93+
exit: vi.fn(),
94+
},
8995
) {
9096
return createDiscordGatewayPlugin({
9197
discordConfig,
92-
runtime: {
93-
log: vi.fn(),
94-
error: vi.fn(),
95-
exit: vi.fn(),
96-
},
98+
runtime,
9799
...(testing ? { testing } : {}),
98100
});
99101
}
@@ -313,4 +315,42 @@ describe("createDiscordGatewayPlugin", () => {
313315

314316
expect(activitySpy).not.toHaveBeenCalled();
315317
});
318+
319+
it("logs Discord gateway websocket error and abnormal close details", () => {
320+
const socket = new EventEmitter() as EventEmitter & { binaryType?: string };
321+
const runtime = {
322+
log: vi.fn(),
323+
error: vi.fn(),
324+
exit: vi.fn(),
325+
};
326+
const plugin = createPlugin(
327+
{
328+
webSocketCtor: function WebSocketCtor() {
329+
return socket;
330+
} as unknown as NonNullable<
331+
Parameters<typeof createDiscordGatewayPlugin>[0]["testing"]
332+
>["webSocketCtor"],
333+
},
334+
{},
335+
runtime,
336+
);
337+
const createdSocket = (
338+
plugin as unknown as { createWebSocket: (url: string) => typeof socket }
339+
).createWebSocket("wss://gateway.discord.gg");
340+
const receiverLimitError = Object.assign(new Error("Too many buffered parts"), {
341+
code: "WS_ERR_TOO_MANY_BUFFERED_PARTS",
342+
});
343+
344+
createdSocket.emit("error", receiverLimitError);
345+
createdSocket.emit("close", 1008, Buffer.from("policy violation"));
346+
347+
const logs = runtime.log.mock.calls.map((call) => String(call[0])).join("\n");
348+
expect(logs).toContain("discord: gateway websocket error");
349+
expect(logs).toContain("code=WS_ERR_TOO_MANY_BUFFERED_PARTS");
350+
expect(logs).toContain("discord: gateway websocket closed");
351+
expect(logs).toContain("code=1008");
352+
expect(logs).toContain("reason=policy violation");
353+
expect(logs).toContain("lastErrorCode=WS_ERR_TOO_MANY_BUFFERED_PARTS");
354+
expect(logs).toContain("hint=possible ws receiver buffered-parts limit");
355+
});
316356
});

extensions/discord/src/monitor/gateway-plugin.ts

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
resolveEffectiveDebugProxyUrl,
99
resolveDebugProxySettings,
1010
} from "openclaw/plugin-sdk/proxy-capture";
11-
import { danger } from "openclaw/plugin-sdk/runtime-env";
11+
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
1212
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
1313
import * as ws from "ws";
1414
import * as discordGateway from "../internal/gateway.js";
@@ -31,6 +31,9 @@ export {
3131
} from "./gateway-metadata.js";
3232

3333
const DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS = 30_000;
34+
const DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE = 1008;
35+
const DISCORD_GATEWAY_WS_RECEIVER_LIMIT_CODE = "WS_ERR_TOO_MANY_BUFFERED_PARTS";
36+
const DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS = 240;
3437
const discordDnsLookup = createDiscordDnsLookup();
3538

3639
type DiscordGatewayWebSocketCtor = new (
@@ -55,6 +58,13 @@ type DiscordGatewayRegistrationState = {
5558
ws?: unknown;
5659
isConnecting?: boolean;
5760
};
61+
type DiscordGatewayTransportErrorDetails = {
62+
name?: string;
63+
message: string;
64+
code?: string;
65+
closeCode?: number;
66+
statusCode?: number;
67+
};
5868

5969
function assignGatewayClient(
6070
plugin: discordGateway.GatewayPlugin,
@@ -68,6 +78,94 @@ function hasGatewaySocketStarted(plugin: discordGateway.GatewayPlugin): boolean
6878
return state.ws != null || state.isConnecting === true;
6979
}
7080

81+
function readStringProperty(value: object, key: string): string | undefined {
82+
const property = (value as Record<string, unknown>)[key];
83+
return typeof property === "string" && property ? property : undefined;
84+
}
85+
86+
function readNumberProperty(value: object, key: string): number | undefined {
87+
const property = (value as Record<string, unknown>)[key];
88+
return typeof property === "number" && Number.isFinite(property) ? property : undefined;
89+
}
90+
91+
function describeDiscordGatewayTransportError(error: Error): DiscordGatewayTransportErrorDetails {
92+
const code = readStringProperty(error, "code");
93+
const closeCode = readNumberProperty(error, "closeCode");
94+
const statusCode = readNumberProperty(error, "statusCode");
95+
return {
96+
...(error.name ? { name: error.name } : {}),
97+
message: error.message,
98+
...(code ? { code } : {}),
99+
...(closeCode !== undefined ? { closeCode } : {}),
100+
...(statusCode !== undefined ? { statusCode } : {}),
101+
};
102+
}
103+
104+
function formatDiscordGatewayCloseReason(reason: Buffer): string {
105+
if (!reason.length) {
106+
return "<empty>";
107+
}
108+
const text = reason.toString("utf8").replaceAll(/\s+/g, " ").trim();
109+
if (!text) {
110+
return `<${reason.length} bytes>`;
111+
}
112+
if (text.length <= DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS) {
113+
return text;
114+
}
115+
return `${text.slice(0, DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS)}...`;
116+
}
117+
118+
function formatDiscordGatewayTransportErrorLog(params: {
119+
flowId: string;
120+
error: DiscordGatewayTransportErrorDetails;
121+
}): string {
122+
const details = [
123+
`flow=${params.flowId}`,
124+
params.error.name ? `name=${params.error.name}` : undefined,
125+
params.error.code ? `code=${params.error.code}` : undefined,
126+
typeof params.error.closeCode === "number" ? `closeCode=${params.error.closeCode}` : undefined,
127+
typeof params.error.statusCode === "number"
128+
? `statusCode=${params.error.statusCode}`
129+
: undefined,
130+
`message=${params.error.message}`,
131+
].filter(Boolean);
132+
return `discord: gateway websocket error ${details.join(" ")}`;
133+
}
134+
135+
function formatDiscordGatewayTransportCloseLog(params: {
136+
flowId: string;
137+
code: number;
138+
reason: Buffer;
139+
lastError?: DiscordGatewayTransportErrorDetails;
140+
}): string {
141+
const receiverLimit =
142+
params.code === DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE ||
143+
params.lastError?.code === DISCORD_GATEWAY_WS_RECEIVER_LIMIT_CODE;
144+
const details = [
145+
`flow=${params.flowId}`,
146+
`code=${params.code}`,
147+
`reasonBytes=${params.reason.length}`,
148+
`reason=${formatDiscordGatewayCloseReason(params.reason)}`,
149+
params.lastError?.code ? `lastErrorCode=${params.lastError.code}` : undefined,
150+
params.lastError?.message ? `lastError=${params.lastError.message}` : undefined,
151+
receiverLimit ? "hint=possible ws receiver buffered-parts limit" : undefined,
152+
].filter(Boolean);
153+
return `discord: gateway websocket closed ${details.join(" ")}`;
154+
}
155+
156+
function shouldLogDiscordGatewayTransportClose(params: {
157+
code: number;
158+
reason: Buffer;
159+
lastError?: DiscordGatewayTransportErrorDetails;
160+
}): boolean {
161+
return (
162+
(params.code !== 1000 && params.code !== 1001) ||
163+
params.reason.length > 0 ||
164+
params.lastError !== undefined ||
165+
params.code === DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE
166+
);
167+
}
168+
71169
type ResolveDiscordGatewayIntentsParams = {
72170
intentsConfig?: import("openclaw/plugin-sdk/config-contracts").DiscordIntentsConfig;
73171
voiceEnabled?: boolean;
@@ -170,6 +268,7 @@ function createGatewayPlugin(params: {
170268
handshakeTimeout: DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS,
171269
...(params.wsAgent ? { agent: params.wsAgent } : {}),
172270
});
271+
let lastTransportError: DiscordGatewayTransportErrorDetails | undefined;
173272
const emitTransportActivity = () => {
174273
if ((this as unknown as { ws?: unknown }).ws !== socket) {
175274
return;
@@ -195,17 +294,37 @@ function createGatewayPlugin(params: {
195294
});
196295
});
197296
socket.on?.("close", (code: number, reason: Buffer) => {
297+
const closeReason = Buffer.isBuffer(reason) ? reason : Buffer.from(String(reason ?? ""));
198298
captureWsEvent({
199299
url,
200300
direction: "local",
201301
kind: "ws-close",
202302
flowId: wsFlowId,
203303
closeCode: code,
204-
payload: reason,
304+
payload: closeReason,
205305
meta: { subsystem: "discord-gateway" },
206306
});
307+
if (
308+
shouldLogDiscordGatewayTransportClose({
309+
code,
310+
reason: closeReason,
311+
lastError: lastTransportError,
312+
})
313+
) {
314+
params.runtime?.log?.(
315+
warn(
316+
formatDiscordGatewayTransportCloseLog({
317+
flowId: wsFlowId,
318+
code,
319+
reason: closeReason,
320+
lastError: lastTransportError,
321+
}),
322+
),
323+
);
324+
}
207325
});
208326
socket.on?.("error", (error: Error) => {
327+
lastTransportError = describeDiscordGatewayTransportError(error);
209328
captureWsEvent({
210329
url,
211330
direction: "local",
@@ -214,6 +333,11 @@ function createGatewayPlugin(params: {
214333
errorText: error.message,
215334
meta: { subsystem: "discord-gateway" },
216335
});
336+
params.runtime?.log?.(
337+
warn(
338+
formatDiscordGatewayTransportErrorLog({ flowId: wsFlowId, error: lastTransportError }),
339+
),
340+
);
217341
});
218342
if ("binaryType" in socket) {
219343
try {

0 commit comments

Comments
 (0)