From 3d7df2bc07471ca68a496964bc2fd84d570d3aee Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 29 May 2026 11:02:31 -0400 Subject: [PATCH] fix(discord): bound delivery retry delays --- extensions/discord/src/delivery-retry.ts | 21 ++++++++------------- extensions/discord/src/retry.test.ts | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/extensions/discord/src/delivery-retry.ts b/extensions/discord/src/delivery-retry.ts index bc2d817f17f..e43b18b630f 100644 --- a/extensions/discord/src/delivery-retry.ts +++ b/extensions/discord/src/delivery-retry.ts @@ -6,6 +6,7 @@ import { } from "openclaw/plugin-sdk/retry-runtime"; import { resolveDiscordAccount } from "./accounts.js"; import { DiscordError } from "./internal/discord.js"; +import { parseDiscordRetryAfterBodySeconds } from "./retry-after.js"; const DISCORD_DELIVERY_RETRY_DEFAULTS = { attempts: 3, @@ -22,27 +23,21 @@ export function isRetryableDiscordDeliveryError(err: unknown): boolean { return status === 429 || (status !== undefined && status >= 500); } -function getDiscordDeliveryRetryAfterMs(err: unknown): number | undefined { +export function getDiscordDeliveryRetryAfterMs(err: unknown): number | undefined { if (!err || typeof err !== "object") { return undefined; } - if ( - "retryAfter" in err && - typeof err.retryAfter === "number" && - Number.isFinite(err.retryAfter) - ) { - return err.retryAfter * 1000; + const retryAfterSeconds = + "retryAfter" in err ? parseDiscordRetryAfterBodySeconds(err.retryAfter) : undefined; + if (retryAfterSeconds !== undefined) { + return retryAfterSeconds * 1000; } const retryAfterRaw = (err as { headers?: Record }).headers?.["retry-after"]; if (!retryAfterRaw) { return undefined; } - const trimmedRetryAfter = retryAfterRaw.trim(); - if (!/^\d+(?:\.\d+)?$/.test(trimmedRetryAfter)) { - return undefined; - } - const retryAfterMs = Number(trimmedRetryAfter) * 1000; - return Number.isFinite(retryAfterMs) ? retryAfterMs : undefined; + const headerSeconds = parseDiscordRetryAfterBodySeconds(retryAfterRaw); + return headerSeconds === undefined ? undefined : headerSeconds * 1000; } export async function withDiscordDeliveryRetry(params: { diff --git a/extensions/discord/src/retry.test.ts b/extensions/discord/src/retry.test.ts index b6cee3292c8..72773666fca 100644 --- a/extensions/discord/src/retry.test.ts +++ b/extensions/discord/src/retry.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it, vi } from "vitest"; -import { isRetryableDiscordDeliveryError } from "./delivery-retry.js"; +import { + getDiscordDeliveryRetryAfterMs, + isRetryableDiscordDeliveryError, +} from "./delivery-retry.js"; import { DiscordError, RateLimitError } from "./internal/discord.js"; import { createDiscordRetryRunner, isRetryableDiscordTransientError } from "./retry.js"; @@ -86,3 +89,17 @@ describe("isRetryableDiscordDeliveryError", () => { expect(isRetryableDiscordDeliveryError(err)).toBe(false); }); }); + +describe("getDiscordDeliveryRetryAfterMs", () => { + it("reads finite retry delays from delivery errors", () => { + expect(getDiscordDeliveryRetryAfterMs({ retryAfter: 0.25 })).toBe(250); + expect(getDiscordDeliveryRetryAfterMs({ headers: { "retry-after": "0.25" } })).toBe(250); + }); + + it("rejects unsafe retry delay magnitudes", () => { + expect(getDiscordDeliveryRetryAfterMs({ retryAfter: 9_007_199_254_741 })).toBeUndefined(); + expect( + getDiscordDeliveryRetryAfterMs({ headers: { "retry-after": "9007199254741" } }), + ).toBeUndefined(); + }); +});