Compare commits

...
Author SHA1 Message Date
ajnart 2db184c1ae style: auto-fix lint and format 2026-07-15 08:39:55 +00:00
ajnart ae891480be fix(releases): log instead of silently swallowing token load failures
The outer catch at releases.ts:79 silently swallowed all errors when
loading widget secrets, including the case where the widget_secret
table didn't exist (migration not applied). This caused tokens to
silently become undefined, leading to unauthenticated API calls and
rate limit errors.

Now logs a warning with the itemId and error so the issue is
diagnosable in server logs instead of failing silently.
2026-07-15 10:38:49 +02:00
ajnart aaff62fff2 fix(releases): return true from rate limit handlers to enable retry
The octokit throttling plugin uses the return value of onRateLimit and
onSecondaryRateLimit to decide whether to retry. Without returning true,
rate-limited requests fail instead of being retried.

Bounded to 1 retry (retryCount < 1) to prevent infinite retry loops.

Addresses CodeRabbit review feedback.
2026-07-14 20:17:19 +02:00
ajnart c01fd723ee fix(releases): add required octokit throttling rate limit handlers
When throttle is enabled in Octokit, the plugin requires onRateLimit
and onSecondaryRateLimit handlers. Without them, the releases widget
crashes with an error when fetching GitHub/DockerHub release data.

Fixes #6309
2026-07-14 20:00:27 +02:00
2 changed files with 34 additions and 5 deletions

No files matched your search

+7 -4
View File
@@ -3,6 +3,7 @@ import { escapeForRegEx } from "@tiptap/react";
import { z } from "zod/v4";
import { decryptSecret } from "@homarr/common/server";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { eq } from "@homarr/db";
import { boards, items, widgetSecrets } from "@homarr/db/schema";
import { releaseProviderKinds } from "@homarr/definitions";
@@ -11,6 +12,8 @@ import { releasesRequestHandler } from "@homarr/request-handler/releases";
import { createTRPCRouter, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
const logger = createLogger({ module: "releasesRouter" });
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
if (!versionFilter) return undefined;
@@ -71,13 +74,13 @@ export const releasesRouter = createTRPCRouter({
for (const secret of secrets) {
try {
tokensByProvider.set(secret.kind, decryptSecret(secret.value));
} catch {
// Skip corrupt secrets
} catch (error) {
logger.warn("Failed to decrypt widget secret", { itemId: input.itemId, kind: secret.kind, error });
}
}
}
} catch {
// Access denied or table not yet migrated -- proceed without tokens
} catch (error) {
logger.warn("Failed to load release widget tokens", { itemId: input.itemId, error });
}
}
@@ -134,7 +134,33 @@ const getGithubApi = (baseUrl: string, userAgent: string, token?: string) =>
baseUrl,
auth: token,
request: { fetch: fetchWithTrustedCertificatesAsync },
throttle: { enabled: true },
throttle: {
enabled: true,
onRateLimit: (
retryAfter: number,
options: { url?: string; method?: string },
_octokit: unknown,
retryCount: number,
) => {
logger.warn(`GitHub rate limit exceeded, retrying after ${retryAfter}s (attempt ${retryCount + 1})`, {
url: options.url,
method: options.method,
});
return retryCount < 1;
},
onSecondaryRateLimit: (
retryAfter: number,
options: { url?: string; method?: string },
_octokit: unknown,
retryCount: number,
) => {
logger.warn(`GitHub secondary rate limit exceeded, retrying after ${retryAfter}s (attempt ${retryCount + 1})`, {
url: options.url,
method: options.method,
});
return retryCount < 1;
},
},
userAgent,
});