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 <steipete@gmail.com>
This commit is contained in:
mushuiyu886
2026-07-16 19:47:36 +08:00
committed by GitHub
parent 48866bbc40
commit dbdd9c93c0
2 changed files with 50 additions and 5 deletions
+37
View File
@@ -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;
+13 -5
View File
@@ -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<T>(params: ClawHubRequestParams): Promise<T> {
const { response, url, hasToken } = await clawhubRequest(params);
if (!response.ok) {