mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
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:
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user