import { extractApiErrorMessage } from "@/utils/apiErrorMessages"; function isAbsoluteHttpUrl(path: string): boolean { return /^https?:\/\//i.test(path); } export async function getApiErrorMessage( response: Response, fallbackMessage: string ): Promise { try { const errorData: unknown = await response.clone().json(); return extractApiErrorMessage(errorData, fallbackMessage, response.status); } catch { try { const text = await response.text(); return extractApiErrorMessage(text, fallbackMessage, response.status); } catch { return fallbackMessage; } } } function withLeadingSlash(path: string): string { return path.startsWith("/") ? path : `/${path}`; } function getConfiguredFastApiUrl(): string | null { if (typeof window !== "undefined" && window.env?.NEXT_PUBLIC_FAST_API) { return window.env.NEXT_PUBLIC_FAST_API; } if (process.env.NEXT_PUBLIC_FAST_API) { return process.env.NEXT_PUBLIC_FAST_API; } return null; } function getFastApiUrlFromQuery(): string | null { if (typeof window === "undefined") return null; try { const params = new URLSearchParams(window.location.search); const value = params.get("fastapiUrl"); if (!value) return null; const parsed = new URL(value); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { return null; } return parsed.origin; } catch { return null; } } function isElectronRuntime(): boolean { return typeof window !== "undefined" && !!window.electron; } function shouldUseDirectFastApiOriginInBrowser(): boolean { return isElectronRuntime() || !!getFastApiUrlFromQuery(); } function resolveBackendPathForRuntime(path: string): string { const normalizedPath = withLeadingSlash(path); // Docker/web runtime should stay same-origin and use nginx reverse proxy. if ( typeof window !== "undefined" && !shouldUseDirectFastApiOriginInBrowser() ) { return normalizedPath; } return `${getFastAPIUrl()}${normalizedPath}`; } // Utility to get the backend base URL. // - Browser web/docker: same origin (nginx proxy). // - Browser electron or query override: direct FastAPI origin. // - Server-side: configured FastAPI origin fallback. export function getFastAPIUrl(): string { const queryFastApiUrl = getFastApiUrlFromQuery(); if (queryFastApiUrl) { return queryFastApiUrl; } if (typeof window !== "undefined") { if (isElectronRuntime()) { return getConfiguredFastApiUrl() || window.location.origin; } return window.location.origin; } return getConfiguredFastApiUrl() || "http://127.0.0.1:5001"; } // Utility to construct API URL for Docker/web runtime. export function getApiUrl(path: string): string { if (isAbsoluteHttpUrl(path)) { return path; } const normalizedPath = withLeadingSlash(path); const isFastApiEndpoint = normalizedPath.startsWith("/api/v1/") || normalizedPath.startsWith("/api/v2/"); if (!isFastApiEndpoint) { return normalizedPath; } if (typeof window === "undefined" && !getConfiguredFastApiUrl()) { return normalizedPath; } return resolveBackendPathForRuntime(normalizedPath); } /** * getApiUrl may return a path without host (e.g. `/api/v1/...`). A single-argument * `new URL("/api/...")` call is invalid; use this before `new URL(..., ...)`-style * builds or to obtain an absolute string for `URL` + `searchParams`. */ export function buildAbsoluteApiRequestUrl( path: string, baseForRelative: string = typeof window !== "undefined" && window.location?.origin ? window.location.origin : "http://127.0.0.1:5001" ): string { const resolved = getApiUrl(path); if (isAbsoluteHttpUrl(resolved)) { return resolved; } return new URL(resolved, baseForRelative).toString(); } function hasBackendAssetPrefix(path: string): boolean { return path.startsWith("/static/") || path.startsWith("/app_data/"); } function toBackendServedPath(rawPath: string): string { const normalized = rawPath.replace(/\\/g, "/"); // Never rewrite Next.js bundled/static assets. if (normalized.startsWith("/_next/static/")) { return normalized; } const appDataIdx = normalized.indexOf("/app_data/"); if (appDataIdx !== -1) { return normalized.slice(appDataIdx); } const staticIdx = normalized.indexOf("/static/"); if (staticIdx !== -1) { return normalized.slice(staticIdx); } const imagesIdx = normalized.lastIndexOf("/images/"); if (imagesIdx !== -1) { return `/app_data${normalized.slice(imagesIdx)}`; } const uploadsIdx = normalized.lastIndexOf("/uploads/"); if (uploadsIdx !== -1) { return `/app_data${normalized.slice(uploadsIdx)}`; } const fontsIdx = normalized.lastIndexOf("/fonts/"); if (fontsIdx !== -1) { return `/app_data${normalized.slice(fontsIdx)}`; } return normalized; } function splitPathAndSuffix(value: string): { path: string; suffix: string } { const hashIdx = value.indexOf("#"); const queryIdx = value.indexOf("?"); const firstSuffixIdx = hashIdx === -1 ? queryIdx : queryIdx === -1 ? hashIdx : Math.min(queryIdx, hashIdx); if (firstSuffixIdx === -1) { return { path: value, suffix: "" }; } return { path: value.slice(0, firstSuffixIdx), suffix: value.slice(firstSuffixIdx), }; } // Resolve backend-served asset paths to the runtime-appropriate backend path. export function resolveBackendAssetUrl(path?: string): string { if (!path) return ""; const trimmedPath = path.trim(); if (!trimmedPath) return ""; if (trimmedPath.startsWith("data:") || trimmedPath.startsWith("blob:")) { return trimmedPath; } if (trimmedPath.startsWith("file:")) { try { const parsed = new URL(trimmedPath); const servedPath = toBackendServedPath(decodeURIComponent(parsed.pathname)); if (hasBackendAssetPrefix(servedPath)) { return resolveBackendPathForRuntime(servedPath); } return trimmedPath; } catch { return trimmedPath; } } if (isAbsoluteHttpUrl(trimmedPath)) { try { const parsed = new URL(trimmedPath); const servedPath = toBackendServedPath(parsed.pathname); if (hasBackendAssetPrefix(servedPath)) { return resolveBackendPathForRuntime( `${servedPath}${parsed.search}${parsed.hash}` ); } return trimmedPath; } catch { return trimmedPath; } } const { path: pathPart, suffix } = splitPathAndSuffix(trimmedPath); const servedPath = toBackendServedPath(withLeadingSlash(pathPart)); if (hasBackendAssetPrefix(servedPath)) { return resolveBackendPathForRuntime(`${servedPath}${suffix}`); } return trimmedPath; } export type BackendAssetLike = { file_url?: string | null; path?: string | null; url?: string | null; }; export function getBackendAssetSource( asset: BackendAssetLike | string | null | undefined ): string { if (typeof asset === "string") { return asset; } if (!asset) { return ""; } return (asset.file_url || asset.path || asset.url || "").trim(); } export function resolveBackendAssetSource( asset: BackendAssetLike | string | null | undefined ): string { return resolveBackendAssetUrl(getBackendAssetSource(asset)); } export const normalizeBackendAssetUrls = (input: T): T => { if (Array.isArray(input)) { return input.map((item) => normalizeBackendAssetUrls(item)) as T; } if (input && typeof input === "object") { const normalized: Record = {}; for (const [key, value] of Object.entries( input as Record )) { normalized[key] = typeof value === "string" ? resolveBackendAssetUrl(value) : normalizeBackendAssetUrls(value); } return normalized as T; } return input; };