From dbdd9c93c0ba653c42fcb65aadf26182298f5bf2 Mon Sep 17 00:00:00 2001 From: mushuiyu886 Date: Thu, 16 Jul 2026 19:47:36 +0800 Subject: [PATCH] fix(clawhub): avoid misleading reset hints from malformed headers (#108815) * fix(clawhub): reject malformed rate-limit reset hints * fix(clawhub): enforce unsigned reset seconds --------- Co-authored-by: Peter Steinberger --- src/infra/clawhub.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/infra/clawhub.ts | 18 +++++++++++++----- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/infra/clawhub.test.ts b/src/infra/clawhub.test.ts index 7c2d55b9d660..265e63395476 100644 --- a/src/infra/clawhub.test.ts +++ b/src/infra/clawhub.test.ts @@ -1016,6 +1016,43 @@ describe("clawhub helpers", () => { ).rejects.toThrow(/Rate limit exceeded Sign in for higher rate limits\.$/); }); + it.each(["0x10", "1e3", "-1", "-0", "+7", "0.5", "9007199254740993"])( + "does not describe malformed RateLimit-Reset values as seconds: %s", + async (reset) => { + process.env.CLAWHUB_CONFIG_PATH = path.join(os.tmpdir(), "openclaw-no-clawhub-config"); + await expect( + searchClawHubSkills({ + query: "calendar", + fetchImpl: async () => + new Response("Rate limit exceeded", { + status: 429, + headers: { "RateLimit-Reset": reset }, + }), + }), + ).rejects.toThrow(/Rate limit exceeded Sign in for higher rate limits\.$/); + }, + ); + + it.each(["invalid", "+7", "-0"])( + "uses a valid Retry-After hint when RateLimit-Reset is malformed: %s", + async (reset) => { + process.env.CLAWHUB_CONFIG_PATH = path.join(os.tmpdir(), "openclaw-no-clawhub-config"); + await expect( + searchClawHubSkills({ + query: "calendar", + fetchImpl: async () => + new Response("Rate limit exceeded", { + status: 429, + headers: { + "RateLimit-Reset": reset, + "Retry-After": "7", + }, + }), + }), + ).rejects.toThrow(/Rate limit exceeded \(resets in 7s\) Sign in for higher rate limits\.$/); + }, + ); + it("retries transient ClawHub reads and honors Retry-After", async () => { const cancel = vi.fn(); let attempts = 0; diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index f67e122855c5..5de02939bcc1 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -718,12 +718,12 @@ async function buildClawHubError( } function formatRateLimitSuffix(headers: Headers, hasToken: boolean): string { - const reset = - normalizeHeaderValue(headers.get("RateLimit-Reset")) ?? - normalizeHeaderValue(headers.get("Retry-After")); + const resetSeconds = + parseRateLimitDeltaSeconds(headers.get("RateLimit-Reset")) ?? + parseRateLimitDeltaSeconds(headers.get("Retry-After")); const segments: string[] = []; - if (reset && Number.isFinite(Number(reset))) { - segments.push(`(resets in ${reset}s)`); + if (resetSeconds !== undefined) { + segments.push(`(resets in ${resetSeconds}s)`); } if (!hasToken) { segments.push("Sign in for higher rate limits."); @@ -731,6 +731,14 @@ function formatRateLimitSuffix(headers: Headers, hasToken: boolean): string { return segments.join(" "); } +function parseRateLimitDeltaSeconds(value: string | null): number | undefined { + const normalized = normalizeHeaderValue(value); + if (!normalized || !/^\d+$/.test(normalized)) { + return undefined; + } + return parseStrictNonNegativeInteger(normalized); +} + async function fetchJson(params: ClawHubRequestParams): Promise { const { response, url, hasToken } = await clawhubRequest(params); if (!response.ok) {