Files
Deep-Package-Inspection/src/lib/api-base.ts
T

75 lines
2.3 KiB
TypeScript

// src/lib/api-base.ts
// Server-only helper: resolve the backend API base URL at runtime.
//
// Why:
// The frontend proxies /api/* to the backend via NEXT_PUBLIC_API_URL /
// INTERNAL_API_URL. In Docker Swarm deployments these may point to a
// task-instance hostname like `backone-inspect-backend-bun-0:3001`. When the
// Swarm service is redeployed the task index can change (`-0` -> `-1`), orphaning
// the configured URL and breaking every proxied request ("fetch failed").
//
// Solution:
// Probe candidates in order and cache the first healthy one. Also auto-derive
// the stable service alias by stripping a trailing `-<digits>` suffix, so a
// stale `-0` URL transparently recovers to the plain service DNS name.
let cachedBase: string | null = null;
function pushUnique(list: string[], value?: string | null) {
if (!value) return;
const clean = value.replace(/\/+$/, '');
if (clean && !list.includes(clean)) list.push(clean);
}
function buildCandidates(): string[] {
const list: string[] = [];
pushUnique(list, process.env.INTERNAL_API_URL);
pushUnique(list, process.env.NEXT_PUBLIC_API_URL);
// Derive stable Swarm service alias (strip task index: ...-0 -> ...)
for (const base of [...list]) {
try {
const u = new URL(base);
const strippedHost = u.host.replace(/-\d+$/, '');
if (strippedHost && strippedHost !== u.host) {
pushUnique(list, `${u.protocol}//${strippedHost}`);
}
} catch {
// ignore malformed URLs
}
}
pushUnique(list, 'http://127.0.0.1:3001');
return list;
}
async function isHealthy(base: string): Promise<boolean> {
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 2500);
const res = await fetch(`${base}/api/health`, { signal: ctrl.signal });
clearTimeout(timer);
return res.ok;
} catch {
return false;
}
}
export async function getApiBase(): Promise<string> {
if (cachedBase) return cachedBase;
for (const base of buildCandidates()) {
if (await isHealthy(base)) {
cachedBase = base;
return base;
}
}
// Fallback: use the first configured candidate even if the probe failed.
cachedBase = buildCandidates()[0] || 'http://127.0.0.1:3001';
return cachedBase;
}
export function resetApiBase() {
cachedBase = null;
}