mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 19:12:22 +00:00
fix(discord): reject unsafe retry-after delays
This commit is contained in:
@@ -103,6 +103,8 @@ describe("fetchDiscord", () => {
|
||||
it.each([
|
||||
["hex", "0x10"],
|
||||
["fractional", "1.5"],
|
||||
["unsafe-ms", "9007199254741"],
|
||||
["unsafe-integer", "9007199254740993"],
|
||||
["overflow", `1${"0".repeat(309)}`],
|
||||
])("rejects invalid Retry-After header values: %s", async (_label, header) => {
|
||||
const fetcher = withFetchPreconnect(
|
||||
@@ -126,6 +128,33 @@ describe("fetchDiscord", () => {
|
||||
expect((error as DiscordApiError).retryAfter).toBe(60);
|
||||
});
|
||||
|
||||
it("ignores unsafe retry_after body values and falls back to Retry-After", async () => {
|
||||
const fetcher = withFetchPreconnect(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
message: "You are being rate limited.",
|
||||
retry_after: 9_007_199_254_741,
|
||||
global: false,
|
||||
}),
|
||||
{ status: 429, headers: { "retry-after": "7" } },
|
||||
),
|
||||
);
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await fetchDiscord("/users/@me/guilds", "test", fetcher, {
|
||||
retry: { attempts: 1 },
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(DiscordApiError);
|
||||
expect((error as DiscordApiError).retryAfter).toBe(7);
|
||||
expect(String(error)).not.toContain("retry after");
|
||||
});
|
||||
|
||||
it("retries rate limits before succeeding", async () => {
|
||||
let calls = 0;
|
||||
const fetcher = withFetchPreconnect(async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type RetryConfig,
|
||||
} from "openclaw/plugin-sdk/retry-runtime";
|
||||
import { isDiscordHtmlResponseBody, summarizeDiscordResponseBody } from "./error-body.js";
|
||||
import { parseRetryAfterHeaderSeconds } from "./retry-after.js";
|
||||
import { parseDiscordRetryAfterBodySeconds, parseRetryAfterHeaderSeconds } from "./retry-after.js";
|
||||
|
||||
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
||||
const DISCORD_API_RETRY_DEFAULTS = {
|
||||
@@ -41,10 +41,7 @@ function parseDiscordApiErrorPayload(text: string): DiscordApiErrorPayload | nul
|
||||
|
||||
function parseRetryAfterSeconds(text: string, response: Response): number | undefined {
|
||||
const payload = parseDiscordApiErrorPayload(text);
|
||||
const retryAfter =
|
||||
payload && typeof payload.retry_after === "number" && Number.isFinite(payload.retry_after)
|
||||
? payload.retry_after
|
||||
: undefined;
|
||||
const retryAfter = parseDiscordRetryAfterBodySeconds(payload?.retry_after);
|
||||
if (retryAfter !== undefined) {
|
||||
return retryAfter;
|
||||
}
|
||||
@@ -88,7 +85,7 @@ function formatDiscordApiErrorText(text: string, response: Response): string | u
|
||||
? payload.message.trim()
|
||||
: "unknown error";
|
||||
const retryAfter = formatRetryAfterSeconds(
|
||||
typeof payload.retry_after === "number" ? payload.retry_after : undefined,
|
||||
parseDiscordRetryAfterBodySeconds(payload.retry_after),
|
||||
);
|
||||
return retryAfter ? `${message} (retry after ${retryAfter})` : message;
|
||||
}
|
||||
|
||||
@@ -620,9 +620,27 @@ describe("RequestClient", () => {
|
||||
await expectRateLimitError(client.get("/channels/c1/messages"), { retryAfter: 7 });
|
||||
});
|
||||
|
||||
it("falls back to Retry-After when the rate limit body value is unsafe", async () => {
|
||||
const client = new RequestClient("test-token", {
|
||||
queueRequests: false,
|
||||
fetch: async () =>
|
||||
new Response(
|
||||
JSON.stringify({ message: "Slow down", retry_after: "9007199254741", global: false }),
|
||||
{
|
||||
status: 429,
|
||||
headers: { "Retry-After": "7" },
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
await expectRateLimitError(client.get("/channels/c1/messages"), { retryAfter: 7 });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["hex", "0x10"],
|
||||
["fractional", "1.5"],
|
||||
["unsafe-ms", "9007199254741"],
|
||||
["unsafe-integer", "9007199254740993"],
|
||||
["overflow", `1${"0".repeat(309)}`],
|
||||
])("rejects invalid Retry-After numeric strings: %s", async (_label, header) => {
|
||||
const client = new RequestClient("test-token", {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import {
|
||||
asFiniteNumberInRange,
|
||||
parseStrictFiniteNumber,
|
||||
parseStrictNonNegativeInteger,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
|
||||
const RETRY_AFTER_HEADER_DELAY_RE = /^\d+$/;
|
||||
const RETRY_AFTER_BODY_SECONDS_RE = /^(?:\d+\.?\d*|\.\d+)$/;
|
||||
const RETRY_AFTER_HTTP_DATE_RE =
|
||||
/^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
||||
const MAX_SAFE_RETRY_AFTER_SECONDS = Number.MAX_SAFE_INTEGER / 1000;
|
||||
|
||||
export function parseRetryAfterHeaderSeconds(
|
||||
value: string | null | undefined,
|
||||
@@ -12,8 +19,10 @@ export function parseRetryAfterHeaderSeconds(
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (RETRY_AFTER_HEADER_DELAY_RE.test(trimmed)) {
|
||||
const delaySeconds = Number(trimmed);
|
||||
return Number.isFinite(delaySeconds) ? delaySeconds : undefined;
|
||||
return asFiniteNumberInRange(parseStrictNonNegativeInteger(trimmed), {
|
||||
min: 0,
|
||||
max: MAX_SAFE_RETRY_AFTER_SECONDS,
|
||||
});
|
||||
}
|
||||
if (!RETRY_AFTER_HTTP_DATE_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
@@ -27,7 +36,7 @@ export function parseDiscordRetryAfterBodySeconds(value: unknown): number | unde
|
||||
typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string" && RETRY_AFTER_BODY_SECONDS_RE.test(value.trim())
|
||||
? Number(value.trim())
|
||||
? parseStrictFiniteNumber(value.trim())
|
||||
: undefined;
|
||||
return seconds !== undefined && Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;
|
||||
return asFiniteNumberInRange(seconds, { min: 0, max: MAX_SAFE_RETRY_AFTER_SECONDS });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user