diff --git a/packages/common/src/url.ts b/packages/common/src/url.ts index 7550def44..f13cc1efc 100644 --- a/packages/common/src/url.ts +++ b/packages/common/src/url.ts @@ -1,7 +1,11 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; export const removeTrailingSlash = (path: string) => { - return path.at(-1) === "/" ? path.substring(0, path.length - 1) : path; + let end = path.length; + while (end > 0 && path.charAt(end - 1) === "/") { + end--; + } + return path.slice(0, end); }; export const extractBaseUrlFromHeaders = ( diff --git a/packages/definitions/src/integration-url-template.ts b/packages/definitions/src/integration-url-template.ts index 62bfa0dea..bb8312c6e 100644 --- a/packages/definitions/src/integration-url-template.ts +++ b/packages/definitions/src/integration-url-template.ts @@ -1,3 +1,5 @@ +import { removeTrailingSlash } from "@homarr/common"; + import { integrationIconSlugs } from "./docker-integration-match"; import type { IntegrationKind } from "./integration"; import { getIntegrationDefaultPort } from "./integration"; @@ -20,7 +22,7 @@ export const buildIntegrationUrl = ( mode: UrlTemplateMode, dockerPort?: number, ): string => { - const host = baseHost.trim().replace(/\/+$/, ""); + const host = removeTrailingSlash(baseHost.trim()); if (!host) return ""; const port = dockerPort ?? getIntegrationDefaultPort(kind); return buildUrl(getSlugForKind(kind), host, mode, port ?? undefined); @@ -32,7 +34,7 @@ export const buildAppUrl = ( mode: UrlTemplateMode, dockerPort?: number, ): string => { - const host = baseHost.trim().replace(/\/+$/, ""); + const host = removeTrailingSlash(baseHost.trim()); if (!host) return ""; const slug = containerName.toLowerCase().replace(/[^a-z0-9-]/g, "-"); return buildUrl(slug, host, mode, dockerPort); diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index e1454bf1f..a56827f15 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -1,4 +1,4 @@ -import { objectKeys } from "@homarr/common"; +import { objectKeys, removeTrailingSlash } from "@homarr/common"; import type { AtLeastOneOf } from "@homarr/common/types"; import { createDocumentationLink } from "./docs"; @@ -475,7 +475,7 @@ export const getIntegrationDefaultPort = (kind: IntegrationKind): number | undef export const getIntegrationApiKeyUrl = (integrationUrl: string, kind: IntegrationKind): string | null => { const definition = integrationDefs[kind]; if (!("apiKeySettingsPath" in definition)) return null; - const base = integrationUrl.replace(/\/+$/, ""); + const base = removeTrailingSlash(integrationUrl); if (!base) return null; return `${base}${definition.apiKeySettingsPath}`; }; diff --git a/packages/definitions/src/test/integration-url-template.spec.ts b/packages/definitions/src/test/integration-url-template.spec.ts index 6ef2b5f2d..05e7a4e9b 100644 --- a/packages/definitions/src/test/integration-url-template.spec.ts +++ b/packages/definitions/src/test/integration-url-template.spec.ts @@ -22,6 +22,7 @@ describe("buildIntegrationUrl", () => { it("strips trailing slashes from host", () => { expect(buildIntegrationUrl("sonarr", "homelab.local/", "subdomain")).toBe("https://sonarr.homelab.local"); + expect(buildIntegrationUrl("sonarr", "homelab.local///", "subdomain")).toBe("https://sonarr.homelab.local"); }); it("returns empty string for empty host", () => { diff --git a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx index c80e1f561..04e9f6f8b 100644 --- a/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx +++ b/packages/modals-collection/src/apps/quick-add-app/quick-add-app-modal.tsx @@ -3,7 +3,7 @@ import type { z } from "zod/v4"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { AppForm } from "@homarr/forms-collection"; -import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals"; +import { createModal, modalSizeForm } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import type { appManageSchema } from "@homarr/validation/app";