fix(discord): clarify deploy abort logs

This commit is contained in:
Peter Steinberger
2026-05-01 07:15:31 +01:00
parent 7340c0322f
commit d23c8a8eba
5 changed files with 133 additions and 5 deletions

View File

@@ -13,6 +13,10 @@ export type DiscordDeployErrorLike = {
scope?: unknown;
rawBody?: unknown;
deployRequestBody?: unknown;
deployRestMethod?: unknown;
deployRestPath?: unknown;
deployRequestMs?: unknown;
deployTimeoutMs?: unknown;
};
export type DiscordDeployRateLimitDetails = {
@@ -32,6 +36,27 @@ export function attachDiscordDeployRequestBody(err: unknown, body: unknown) {
}
}
export function attachDiscordDeployRestContext(
err: unknown,
context: {
method: string;
path: string;
requestMs: number;
timeoutMs?: number;
},
) {
if (!err || typeof err !== "object") {
return;
}
const deployErr = err as DiscordDeployErrorLike;
deployErr.deployRestMethod = context.method;
deployErr.deployRestPath = context.path;
deployErr.deployRequestMs = context.requestMs;
if (typeof context.timeoutMs === "number" && Number.isFinite(context.timeoutMs)) {
deployErr.deployTimeoutMs = context.timeoutMs;
}
}
function stringifyDiscordDeployField(value: unknown): string {
if (typeof value === "string") {
return JSON.stringify(value);
@@ -82,6 +107,78 @@ function readFiniteNumber(value: unknown): number | undefined {
return undefined;
}
function formatDurationMs(ms: number): string {
return formatDurationSeconds(ms, { decimals: ms >= 1000 ? 1 : 0 });
}
function isAbortLikeError(err: unknown): boolean {
if (!err || typeof err !== "object") {
return false;
}
const name = "name" in err && typeof err.name === "string" ? err.name : undefined;
const message = formatErrorMessage(err);
return (
name === "AbortError" ||
message === "This operation was aborted" ||
message === "The operation was aborted" ||
/\boperation was aborted\b/i.test(message)
);
}
function formatDiscordDeployRestOperation(err: DiscordDeployErrorLike): string {
const method =
typeof err.deployRestMethod === "string" && err.deployRestMethod.trim().length > 0
? err.deployRestMethod.toUpperCase()
: undefined;
const path =
typeof err.deployRestPath === "string" && err.deployRestPath.trim().length > 0
? err.deployRestPath
: undefined;
if (method && path) {
return `${method} ${path}`;
}
if (method) {
return method;
}
if (path) {
return path;
}
return "request";
}
export function formatDiscordDeployErrorMessage(err: unknown): string {
if (!isAbortLikeError(err)) {
return formatErrorMessage(err);
}
const deployErr =
err && typeof err === "object"
? (err as DiscordDeployErrorLike)
: ({} as DiscordDeployErrorLike);
const requestMs = readFiniteNumber(deployErr.deployRequestMs);
const timeoutMs = readFiniteNumber(deployErr.deployTimeoutMs);
const operation = formatDiscordDeployRestOperation(deployErr);
const hasRestContext =
requestMs !== undefined ||
timeoutMs !== undefined ||
deployErr.deployRestMethod !== undefined ||
deployErr.deployRestPath !== undefined;
if (!hasRestContext) {
return "Discord REST request was aborted";
}
const timing: string[] = [];
if (timeoutMs !== undefined) {
timing.push(`timeout=${formatDurationMs(timeoutMs)}`);
}
if (requestMs !== undefined) {
timing.push(`observed=${formatDurationMs(requestMs)}`);
}
const timingText = timing.length > 0 ? ` (${timing.join(", ")})` : "";
if (timeoutMs !== undefined && requestMs !== undefined && requestMs >= timeoutMs) {
return `Discord REST ${operation} timed out${timingText}`;
}
return `Discord REST ${operation} was aborted${timingText}`;
}
export function resolveDiscordDeployRateLimitDetails(
err: unknown,
): DiscordDeployRateLimitDetails | undefined {

View File

@@ -2,8 +2,10 @@ import { formatDurationSeconds, warn, type RuntimeEnv } from "openclaw/plugin-sd
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { Client, overwriteApplicationCommands, type RequestClient } from "../internal/discord.js";
import {
attachDiscordDeployRestContext,
attachDiscordDeployRequestBody,
formatDiscordDeployErrorDetails,
formatDiscordDeployErrorMessage,
formatDiscordDeployRateLimitDetails,
formatDiscordDeployRateLimitWarning,
isDiscordDeployDailyCreateLimit,
@@ -27,6 +29,7 @@ function wrapDeployRestMethod(params: {
runtime: RuntimeEnv;
accountId: string;
startupStartedAt: number;
timeoutMs?: number;
shouldLogVerbose: () => boolean;
}) {
return async (path: string, data?: never, query?: never) => {
@@ -51,20 +54,27 @@ function wrapDeployRestMethod(params: {
}
return result;
} catch (err) {
const requestMs = Date.now() - startedAt;
attachDiscordDeployRequestBody(err, body);
attachDiscordDeployRestContext(err, {
method: params.method,
path,
requestMs,
timeoutMs: params.timeoutMs,
});
const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
if (rateLimitDetails) {
if (params.shouldLogVerbose()) {
params.runtime.log?.(
warn(
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}${rateLimitDetails}`,
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs}${rateLimitDetails}`,
),
);
}
} else {
const details = formatDiscordDeployErrorDetails(err);
params.runtime.error?.(
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt} error=${formatErrorMessage(err)}${details}`,
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs} error=${formatDiscordDeployErrorMessage(err)}${details}`,
);
}
throw err;
@@ -87,12 +97,14 @@ function installDeployRestLogging(params: {
delete: params.rest.delete.bind(params.rest),
};
for (const method of Object.keys(original) as RestMethodName[]) {
const timeout = (params.rest as { options?: { timeout?: unknown } }).options?.timeout;
params.rest[method] = wrapDeployRestMethod({
method,
original,
runtime: params.runtime,
accountId: params.accountId,
startupStartedAt: params.startupStartedAt,
timeoutMs: typeof timeout === "number" ? timeout : undefined,
shouldLogVerbose: params.shouldLogVerbose,
}) as RequestClient[typeof method];
}
@@ -168,7 +180,7 @@ export async function deployDiscordCommands(params: {
params.runtime.log?.(
warn(
rateLimitWarning ??
`discord: native slash command deploy warning (not message send): ${formatErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
),
);
} finally {

View File

@@ -792,11 +792,25 @@ describe("monitorDiscordProvider", () => {
.mock.calls.some(
(call) =>
String(call[0]).includes("native slash command deploy warning (not message send):") &&
String(call[0]).includes("This operation was aborted"),
String(call[0]).includes("Discord REST request was aborted"),
),
).toBe(true);
});
it("formats native command deploy aborts with REST timeout context", () => {
const error = Object.assign(new Error("This operation was aborted"), {
name: "AbortError",
deployRestMethod: "patch",
deployRestPath: "/applications/app-1/commands/cmd-1",
deployRequestMs: 24_657,
deployTimeoutMs: 15_000,
});
expect(providerTesting.formatDiscordDeployErrorMessage(error)).toBe(
"Discord REST PATCH /applications/app-1/commands/cmd-1 timed out (timeout=15s, observed=24.7s)",
);
});
it("logs repeated native command deploy rate limits as one concise warning", async () => {
const runtime = baseRuntime();
const rateLimitError = createRateLimitError(

View File

@@ -47,7 +47,10 @@ import {
type GetPluginCommandSpecs,
} from "./provider.commands.js";
import { logDiscordResolvedConfig } from "./provider.config-log.js";
import { formatDiscordDeployErrorDetails } from "./provider.deploy-errors.js";
import {
formatDiscordDeployErrorDetails,
formatDiscordDeployErrorMessage,
} from "./provider.deploy-errors.js";
import {
clearDiscordNativeCommands,
runDiscordCommandDeployInBackground,
@@ -644,6 +647,7 @@ export const __testing = {
resolveDiscordRestFetch,
resolveThreadBindingsEnabled: resolveThreadBindingsEnabledForTesting,
formatDiscordDeployErrorDetails,
formatDiscordDeployErrorMessage,
setFetchDiscordApplicationId(mock?: typeof fetchDiscordApplicationId) {
fetchDiscordApplicationIdForTesting = mock;
},