From c80e9878de86513dc66fbdc20c325f92422cc1cc Mon Sep 17 00:00:00 2001 From: ajnart Date: Wed, 29 Jul 2026 07:23:09 +0200 Subject: [PATCH] fix: handle lazy startup failures --- .../components/board/items/item-content.tsx | 39 ++++++++++++------- .../integration-multi-select-grid.tsx | 15 ++++++- .../integration/integration-select-grid.tsx | 15 ++++++- apps/nextjs/src/instrumentation.spec.ts | 18 +++++++++ apps/nextjs/src/instrumentation.ts | 19 ++++++++- 5 files changed, 85 insertions(+), 21 deletions(-) diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index aece414cf..c40b8a57a 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -5,6 +5,7 @@ import { useElementSize } from "@mantine/hooks"; import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query"; import combineClasses from "clsx"; import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes"; +import type { FallbackProps } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary"; import { useRequiredBoard } from "@homarr/boards/context"; @@ -33,6 +34,12 @@ const getOverflowFromKind = (kind: SectionItem["kind"]) => { return undefined; }; +const WidgetDefinitionLoadError = ({ error, resetErrorBoundary }: FallbackProps) => ( +
+ +
+); + export const BoardItemContent = ({ item }: BoardItemContentProps) => { const { ref, width, height } = useElementSize(); const board = useRequiredBoard(); @@ -40,21 +47,23 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => { return ( <> - - - - } - > - - + + + + + } + > + + + {item.advancedOptions.title?.trim() && ( buildSortedIntegrations({ enableMockIntegration, onboarding }), }); @@ -136,7 +140,14 @@ export const IntegrationMultiSelectGrid = ({ ); })} - {!isPending && filtered.length === 0 && ( + {isError && ( +
+ + {t("common.error")} + +
+ )} + {!isPending && !isError && filtered.length === 0 && (
{t("common.noResults")}
diff --git a/apps/nextjs/src/components/integration/integration-select-grid.tsx b/apps/nextjs/src/components/integration/integration-select-grid.tsx index 017f0acf5..e6458df6e 100644 --- a/apps/nextjs/src/components/integration/integration-select-grid.tsx +++ b/apps/nextjs/src/components/integration/integration-select-grid.tsx @@ -20,7 +20,11 @@ export const IntegrationSelectGrid = ({ onSelect, enableMockIntegration = false const [search, setSearch] = useState(""); const t = useI18n(); - const { data: integrations = [], isPending } = useQuery({ + const { + data: integrations = [], + isPending, + isError, + } = useQuery({ queryKey: ["widget-integrations", { enableMockIntegration }], queryFn: () => buildSortedIntegrations({ enableMockIntegration }), }); @@ -89,7 +93,14 @@ export const IntegrationSelectGrid = ({ onSelect, enableMockIntegration = false ))} - {!isPending && filtered.length === 0 && ( + {isError && ( +
+ + {t("common.error")} + +
+ )} + {!isPending && !isError && filtered.length === 0 && (
{t("common.noResults")}
diff --git a/apps/nextjs/src/instrumentation.spec.ts b/apps/nextjs/src/instrumentation.spec.ts index 384e3ddad..c8cb5d586 100644 --- a/apps/nextjs/src/instrumentation.spec.ts +++ b/apps/nextjs/src/instrumentation.spec.ts @@ -52,4 +52,22 @@ describe("Next instrumentation", () => { expect(tasksStarted).not.toHaveBeenCalled(); expect(websocketStarted).not.toHaveBeenCalled(); }); + + test("isolates embedded service startup failures", async () => { + const websocketStarted = vi.fn(); + vi.stubEnv("NODE_ENV", "production"); + vi.stubEnv("NEXT_RUNTIME", "nodejs"); + vi.doMock("@homarr/tasks", () => { + throw new Error("task startup failed"); + }); + vi.doMock("@homarr/websocket", () => { + websocketStarted(); + return {}; + }); + + const { register } = await import("./instrumentation"); + + await expect(register()).resolves.toBeUndefined(); + expect(websocketStarted).toHaveBeenCalledOnce(); + }); }); diff --git a/apps/nextjs/src/instrumentation.ts b/apps/nextjs/src/instrumentation.ts index d6d55f3fc..d73053bec 100644 --- a/apps/nextjs/src/instrumentation.ts +++ b/apps/nextjs/src/instrumentation.ts @@ -1,7 +1,22 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; + +const logger = createLogger({ module: "instrumentation" }); + export async function register() { if (process.env.NEXT_RUNTIME !== "nodejs") return; if (process.env.NODE_ENV !== "production") return; - await import("@homarr/tasks"); - await import("@homarr/websocket"); + const services = [ + { name: "tasks", start: () => import("@homarr/tasks") }, + { name: "websocket", start: () => import("@homarr/websocket") }, + ]; + const results = await Promise.allSettled(services.map(({ start }) => start())); + + for (const [index, result] of results.entries()) { + if (result.status === "rejected") { + logger.error( + new Error(`Failed to start embedded ${services[index]?.name ?? "unknown"} service`, { cause: result.reason }), + ); + } + } }