293 lines
7.7 KiB
TypeScript
293 lines
7.7 KiB
TypeScript
import { extractApiErrorMessage } from "@/utils/apiErrorMessages";
|
|
|
|
function isAbsoluteHttpUrl(path: string): boolean {
|
|
return /^https?:\/\//i.test(path);
|
|
}
|
|
|
|
export async function getApiErrorMessage(
|
|
response: Response,
|
|
fallbackMessage: string
|
|
): Promise<string> {
|
|
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 = <T,>(input: T): T => {
|
|
if (Array.isArray(input)) {
|
|
return input.map((item) => normalizeBackendAssetUrls(item)) as T;
|
|
}
|
|
|
|
if (input && typeof input === "object") {
|
|
const normalized: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(
|
|
input as Record<string, unknown>
|
|
)) {
|
|
normalized[key] =
|
|
typeof value === "string"
|
|
? resolveBackendAssetUrl(value)
|
|
: normalizeBackendAssetUrls(value);
|
|
}
|
|
return normalized as T;
|
|
}
|
|
|
|
return input;
|
|
};
|