mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-26 23:29:31 +00:00
fix(release): drain rate-limited ClawHub responses
This commit is contained in:
@@ -90,6 +90,7 @@ const CLAWHUB_DEFAULT_REGISTRY = "https://clawhub.ai";
|
||||
const CLAWHUB_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 64 * 1024;
|
||||
const CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS = [1_000, 3_000, 10_000] as const;
|
||||
const CLAWHUB_MAX_RETRY_AFTER_MS = 60_000;
|
||||
const OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY = "openclaw/openclaw";
|
||||
const OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME = "plugin-clawhub-release.yml";
|
||||
const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
||||
@@ -490,8 +491,10 @@ async function hasClawHubTrustedPublisher(
|
||||
});
|
||||
const { response } = request;
|
||||
|
||||
const retryRateLimit =
|
||||
response.status === 429 && attempt < CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS.length;
|
||||
try {
|
||||
if (response.status !== 429 || attempt >= CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS.length) {
|
||||
if (!retryRateLimit) {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to query ClawHub trusted publisher for ${packageName}: ${response.status} ${response.statusText}`,
|
||||
@@ -522,20 +525,27 @@ async function hasClawHubTrustedPublisher(
|
||||
request.clearTimeout();
|
||||
}
|
||||
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
await delay(clawHubRetryDelayMs(response, attempt));
|
||||
}
|
||||
}
|
||||
|
||||
function clawHubRetryDelayMs(response: Response, attempt: number): number {
|
||||
const retryAfter = response.headers.get("retry-after");
|
||||
if (retryAfter !== null) {
|
||||
const retryAfter = response.headers.get("retry-after")?.trim();
|
||||
if (retryAfter) {
|
||||
const retryAfterSeconds = Number(retryAfter);
|
||||
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) {
|
||||
return Math.round(retryAfterSeconds * 1_000);
|
||||
const retryAfterMs = Math.round(retryAfterSeconds * 1_000);
|
||||
if (retryAfterMs <= CLAWHUB_MAX_RETRY_AFTER_MS) {
|
||||
return retryAfterMs;
|
||||
}
|
||||
}
|
||||
const retryAfterAt = Date.parse(retryAfter);
|
||||
if (Number.isFinite(retryAfterAt)) {
|
||||
return Math.max(0, retryAfterAt - Date.now());
|
||||
const retryAfterMs = Math.max(0, retryAfterAt - Date.now());
|
||||
if (retryAfterMs <= CLAWHUB_MAX_RETRY_AFTER_MS) {
|
||||
return retryAfterMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
return CLAWHUB_RATE_LIMIT_RETRY_DELAYS_MS[attempt] ?? 0;
|
||||
|
||||
@@ -455,6 +455,7 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
it("retries a rate-limited trusted publisher lookup", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
let trustedPublisherRequests = 0;
|
||||
let rateLimitedBodyCanceled = false;
|
||||
let firstTrustedPublisherRequestAt: number | undefined;
|
||||
let retryTrustedPublisherRequestAt: number | undefined;
|
||||
const fetchImpl: typeof fetch = async (input) => {
|
||||
@@ -468,7 +469,14 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
trustedPublisherRequests += 1;
|
||||
if (trustedPublisherRequests === 1) {
|
||||
firstTrustedPublisherRequestAt = Date.now();
|
||||
return new Response("", { status: 429 });
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
cancel() {
|
||||
rateLimitedBodyCanceled = true;
|
||||
},
|
||||
}),
|
||||
{ status: 429 },
|
||||
);
|
||||
}
|
||||
retryTrustedPublisherRequestAt = Date.now();
|
||||
return new Response(
|
||||
@@ -495,6 +503,7 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
});
|
||||
|
||||
expect(trustedPublisherRequests).toBe(2);
|
||||
expect(rateLimitedBodyCanceled).toBe(true);
|
||||
expect(retryTrustedPublisherRequestAt).toBeGreaterThanOrEqual(
|
||||
(firstTrustedPublisherRequestAt ?? Number.POSITIVE_INFINITY) + 900,
|
||||
);
|
||||
@@ -555,6 +564,54 @@ describe("collectPluginClawHubReleasePlan", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the bounded retry schedule for an excessive Retry-After header", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
let trustedPublisherRequests = 0;
|
||||
let firstTrustedPublisherRequestAt: number | undefined;
|
||||
let retryTrustedPublisherRequestAt: number | undefined;
|
||||
const fetchImpl: typeof fetch = async (input) => {
|
||||
const requestUrl =
|
||||
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
const pathname = new URL(requestUrl).pathname;
|
||||
if (pathname === "/api/v1/packages/%40openclaw%2Fdemo-plugin") {
|
||||
return new Response("{}", { status: 200 });
|
||||
}
|
||||
if (pathname === "/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher") {
|
||||
trustedPublisherRequests += 1;
|
||||
if (trustedPublisherRequests === 1) {
|
||||
firstTrustedPublisherRequestAt = Date.now();
|
||||
return new Response("", { status: 429, headers: { "retry-after": "999999999999" } });
|
||||
}
|
||||
retryTrustedPublisherRequestAt = Date.now();
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
trustedPublisher: {
|
||||
repository: "openclaw/openclaw",
|
||||
workflowFilename: "plugin-clawhub-release.yml",
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (pathname === "/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/2026.4.1") {
|
||||
return new Response("", { status: 404 });
|
||||
}
|
||||
throw new Error(`Unexpected ClawHub request to ${pathname}`);
|
||||
};
|
||||
|
||||
await collectPluginClawHubReleasePlan({
|
||||
rootDir: repoDir,
|
||||
selection: ["@openclaw/demo-plugin"],
|
||||
fetchImpl,
|
||||
registryBaseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(trustedPublisherRequests).toBe(2);
|
||||
expect(retryTrustedPublisherRequestAt).toBeGreaterThanOrEqual(
|
||||
(firstTrustedPublisherRequestAt ?? Number.POSITIVE_INFINITY) + 900,
|
||||
);
|
||||
});
|
||||
|
||||
it("routes missing package rows to bootstrap candidates instead of normal candidates", async () => {
|
||||
const repoDir = createTempPluginRepo();
|
||||
const { fetchImpl } = createClawHubPlanFetch({
|
||||
|
||||
Reference in New Issue
Block a user