mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(discord): log gateway websocket close details
This commit is contained in:
@@ -66,6 +66,7 @@ vi.mock("openclaw/plugin-sdk/proxy-capture", () => ({
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
danger: (value: string) => value,
|
||||
warn: (value: string) => value,
|
||||
}));
|
||||
|
||||
describe("createDiscordGatewayPlugin", () => {
|
||||
@@ -86,14 +87,15 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
function createPlugin(
|
||||
testing?: NonNullable<Parameters<typeof createDiscordGatewayPlugin>[0]["testing"]>,
|
||||
discordConfig: Parameters<typeof createDiscordGatewayPlugin>[0]["discordConfig"] = {},
|
||||
runtime: Parameters<typeof createDiscordGatewayPlugin>[0]["runtime"] = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
) {
|
||||
return createDiscordGatewayPlugin({
|
||||
discordConfig,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
runtime,
|
||||
...(testing ? { testing } : {}),
|
||||
});
|
||||
}
|
||||
@@ -313,4 +315,42 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
|
||||
expect(activitySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs Discord gateway websocket error and abnormal close details", () => {
|
||||
const socket = new EventEmitter() as EventEmitter & { binaryType?: string };
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const plugin = createPlugin(
|
||||
{
|
||||
webSocketCtor: function WebSocketCtor() {
|
||||
return socket;
|
||||
} as unknown as NonNullable<
|
||||
Parameters<typeof createDiscordGatewayPlugin>[0]["testing"]
|
||||
>["webSocketCtor"],
|
||||
},
|
||||
{},
|
||||
runtime,
|
||||
);
|
||||
const createdSocket = (
|
||||
plugin as unknown as { createWebSocket: (url: string) => typeof socket }
|
||||
).createWebSocket("wss://gateway.discord.gg");
|
||||
const receiverLimitError = Object.assign(new Error("Too many buffered parts"), {
|
||||
code: "WS_ERR_TOO_MANY_BUFFERED_PARTS",
|
||||
});
|
||||
|
||||
createdSocket.emit("error", receiverLimitError);
|
||||
createdSocket.emit("close", 1008, Buffer.from("policy violation"));
|
||||
|
||||
const logs = runtime.log.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(logs).toContain("discord: gateway websocket error");
|
||||
expect(logs).toContain("code=WS_ERR_TOO_MANY_BUFFERED_PARTS");
|
||||
expect(logs).toContain("discord: gateway websocket closed");
|
||||
expect(logs).toContain("code=1008");
|
||||
expect(logs).toContain("reason=policy violation");
|
||||
expect(logs).toContain("lastErrorCode=WS_ERR_TOO_MANY_BUFFERED_PARTS");
|
||||
expect(logs).toContain("hint=possible ws receiver buffered-parts limit");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
resolveEffectiveDebugProxyUrl,
|
||||
resolveDebugProxySettings,
|
||||
} from "openclaw/plugin-sdk/proxy-capture";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import * as ws from "ws";
|
||||
import * as discordGateway from "../internal/gateway.js";
|
||||
@@ -31,6 +31,9 @@ export {
|
||||
} from "./gateway-metadata.js";
|
||||
|
||||
const DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS = 30_000;
|
||||
const DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE = 1008;
|
||||
const DISCORD_GATEWAY_WS_RECEIVER_LIMIT_CODE = "WS_ERR_TOO_MANY_BUFFERED_PARTS";
|
||||
const DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS = 240;
|
||||
const discordDnsLookup = createDiscordDnsLookup();
|
||||
|
||||
type DiscordGatewayWebSocketCtor = new (
|
||||
@@ -55,6 +58,13 @@ type DiscordGatewayRegistrationState = {
|
||||
ws?: unknown;
|
||||
isConnecting?: boolean;
|
||||
};
|
||||
type DiscordGatewayTransportErrorDetails = {
|
||||
name?: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
closeCode?: number;
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
function assignGatewayClient(
|
||||
plugin: discordGateway.GatewayPlugin,
|
||||
@@ -68,6 +78,94 @@ function hasGatewaySocketStarted(plugin: discordGateway.GatewayPlugin): boolean
|
||||
return state.ws != null || state.isConnecting === true;
|
||||
}
|
||||
|
||||
function readStringProperty(value: object, key: string): string | undefined {
|
||||
const property = (value as Record<string, unknown>)[key];
|
||||
return typeof property === "string" && property ? property : undefined;
|
||||
}
|
||||
|
||||
function readNumberProperty(value: object, key: string): number | undefined {
|
||||
const property = (value as Record<string, unknown>)[key];
|
||||
return typeof property === "number" && Number.isFinite(property) ? property : undefined;
|
||||
}
|
||||
|
||||
function describeDiscordGatewayTransportError(error: Error): DiscordGatewayTransportErrorDetails {
|
||||
const code = readStringProperty(error, "code");
|
||||
const closeCode = readNumberProperty(error, "closeCode");
|
||||
const statusCode = readNumberProperty(error, "statusCode");
|
||||
return {
|
||||
...(error.name ? { name: error.name } : {}),
|
||||
message: error.message,
|
||||
...(code ? { code } : {}),
|
||||
...(closeCode !== undefined ? { closeCode } : {}),
|
||||
...(statusCode !== undefined ? { statusCode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function formatDiscordGatewayCloseReason(reason: Buffer): string {
|
||||
if (!reason.length) {
|
||||
return "<empty>";
|
||||
}
|
||||
const text = reason.toString("utf8").replaceAll(/\s+/g, " ").trim();
|
||||
if (!text) {
|
||||
return `<${reason.length} bytes>`;
|
||||
}
|
||||
if (text.length <= DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, DISCORD_GATEWAY_CLOSE_REASON_LOG_MAX_CHARS)}...`;
|
||||
}
|
||||
|
||||
function formatDiscordGatewayTransportErrorLog(params: {
|
||||
flowId: string;
|
||||
error: DiscordGatewayTransportErrorDetails;
|
||||
}): string {
|
||||
const details = [
|
||||
`flow=${params.flowId}`,
|
||||
params.error.name ? `name=${params.error.name}` : undefined,
|
||||
params.error.code ? `code=${params.error.code}` : undefined,
|
||||
typeof params.error.closeCode === "number" ? `closeCode=${params.error.closeCode}` : undefined,
|
||||
typeof params.error.statusCode === "number"
|
||||
? `statusCode=${params.error.statusCode}`
|
||||
: undefined,
|
||||
`message=${params.error.message}`,
|
||||
].filter(Boolean);
|
||||
return `discord: gateway websocket error ${details.join(" ")}`;
|
||||
}
|
||||
|
||||
function formatDiscordGatewayTransportCloseLog(params: {
|
||||
flowId: string;
|
||||
code: number;
|
||||
reason: Buffer;
|
||||
lastError?: DiscordGatewayTransportErrorDetails;
|
||||
}): string {
|
||||
const receiverLimit =
|
||||
params.code === DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE ||
|
||||
params.lastError?.code === DISCORD_GATEWAY_WS_RECEIVER_LIMIT_CODE;
|
||||
const details = [
|
||||
`flow=${params.flowId}`,
|
||||
`code=${params.code}`,
|
||||
`reasonBytes=${params.reason.length}`,
|
||||
`reason=${formatDiscordGatewayCloseReason(params.reason)}`,
|
||||
params.lastError?.code ? `lastErrorCode=${params.lastError.code}` : undefined,
|
||||
params.lastError?.message ? `lastError=${params.lastError.message}` : undefined,
|
||||
receiverLimit ? "hint=possible ws receiver buffered-parts limit" : undefined,
|
||||
].filter(Boolean);
|
||||
return `discord: gateway websocket closed ${details.join(" ")}`;
|
||||
}
|
||||
|
||||
function shouldLogDiscordGatewayTransportClose(params: {
|
||||
code: number;
|
||||
reason: Buffer;
|
||||
lastError?: DiscordGatewayTransportErrorDetails;
|
||||
}): boolean {
|
||||
return (
|
||||
(params.code !== 1000 && params.code !== 1001) ||
|
||||
params.reason.length > 0 ||
|
||||
params.lastError !== undefined ||
|
||||
params.code === DISCORD_GATEWAY_POLICY_VIOLATION_CLOSE_CODE
|
||||
);
|
||||
}
|
||||
|
||||
type ResolveDiscordGatewayIntentsParams = {
|
||||
intentsConfig?: import("openclaw/plugin-sdk/config-contracts").DiscordIntentsConfig;
|
||||
voiceEnabled?: boolean;
|
||||
@@ -170,6 +268,7 @@ function createGatewayPlugin(params: {
|
||||
handshakeTimeout: DISCORD_GATEWAY_HANDSHAKE_TIMEOUT_MS,
|
||||
...(params.wsAgent ? { agent: params.wsAgent } : {}),
|
||||
});
|
||||
let lastTransportError: DiscordGatewayTransportErrorDetails | undefined;
|
||||
const emitTransportActivity = () => {
|
||||
if ((this as unknown as { ws?: unknown }).ws !== socket) {
|
||||
return;
|
||||
@@ -195,17 +294,37 @@ function createGatewayPlugin(params: {
|
||||
});
|
||||
});
|
||||
socket.on?.("close", (code: number, reason: Buffer) => {
|
||||
const closeReason = Buffer.isBuffer(reason) ? reason : Buffer.from(String(reason ?? ""));
|
||||
captureWsEvent({
|
||||
url,
|
||||
direction: "local",
|
||||
kind: "ws-close",
|
||||
flowId: wsFlowId,
|
||||
closeCode: code,
|
||||
payload: reason,
|
||||
payload: closeReason,
|
||||
meta: { subsystem: "discord-gateway" },
|
||||
});
|
||||
if (
|
||||
shouldLogDiscordGatewayTransportClose({
|
||||
code,
|
||||
reason: closeReason,
|
||||
lastError: lastTransportError,
|
||||
})
|
||||
) {
|
||||
params.runtime?.log?.(
|
||||
warn(
|
||||
formatDiscordGatewayTransportCloseLog({
|
||||
flowId: wsFlowId,
|
||||
code,
|
||||
reason: closeReason,
|
||||
lastError: lastTransportError,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
socket.on?.("error", (error: Error) => {
|
||||
lastTransportError = describeDiscordGatewayTransportError(error);
|
||||
captureWsEvent({
|
||||
url,
|
||||
direction: "local",
|
||||
@@ -214,6 +333,11 @@ function createGatewayPlugin(params: {
|
||||
errorText: error.message,
|
||||
meta: { subsystem: "discord-gateway" },
|
||||
});
|
||||
params.runtime?.log?.(
|
||||
warn(
|
||||
formatDiscordGatewayTransportErrorLog({ flowId: wsFlowId, error: lastTransportError }),
|
||||
),
|
||||
);
|
||||
});
|
||||
if ("binaryType" in socket) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user