import { getAccessToken } from "./auth";
const DEFAULT_TIMEOUT_MS = Number(import.meta.env.VITE_API_TIMEOUT_MS || 10000);
// Logger for HTTP requests — console-only, no server sync
const LOG_PREFIX = "[vue:http]";
const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
const MIN_LEVEL = import.meta.env.VITE_LOG_LEVEL
? LOG_LEVELS[import.meta.env.VITE_LOG_LEVEL.toUpperCase()] ?? LOG_LEVELS.INFO
: LOG_LEVELS.INFO;
function shouldLog(level) {
return LOG_LEVELS[level] >= MIN_LEVEL;
}
function formatDuration(ms) {
return ms < 10 ? `${ms.toFixed(1)}ms` : `${Math.round(ms)}ms`;
}
function truncate(str, maxLen = 500) {
if (!str) return "";
const s = String(str);
return s.length > maxLen ? s.slice(0, maxLen) + "…" : s;
}
function buildQuery(params) {
const query = new URLSearchParams();
for (const [key, value] of Object.entries(params || {})) {
if (value === undefined || value === null) {
continue;
}
query.append(key, String(value));
}
const serialized = query.toString();
return serialized ? `?${serialized}` : "";
}
function joinUrl(baseUrl, path) {
const base = String(baseUrl || "").replace(/\/+$/, "");
const nextPath = String(path || "").replace(/^\/+/, "");
if (!base) {
return `/${nextPath}`;
}
return `${base}/${nextPath}`;
}
function wrapProxyPath(path, query) {
const proxyPath = import.meta.env.VITE_API_PROXY_PATH || "";
if (!proxyPath) {
return `${path}${buildQuery(query)}`;
}
return `${proxyPath}${buildQuery({ path, ...(query || {}) })}`;
}
export async function requestHttp(method, path, body, options = {}) {
const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const baseUrl = import.meta.env.VITE_API_BASE_URL || "";
const url = joinUrl(baseUrl, wrapProxyPath(path, options.query));
const headers = {
Accept: "application/json",
...(options.headers || {}),
};
const token = getAccessToken();
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const init = {
method,
headers,
signal: controller.signal,
credentials: "include",
};
if (options.signal) {
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
}
if (body !== undefined && body !== null) {
headers["Content-Type"] = "application/json";
init.body = JSON.stringify(body);
}
// Log request start
if (shouldLog("DEBUG")) {
console.debug(LOG_PREFIX, method, url, body ? truncate(JSON.stringify(body)) : "");
}
const startTime = performance.now();
try {
const response = await fetch(url, init);
const text = await response.text();
let data = text;
if (text) {
try {
data = JSON.parse(text);
} catch (_) {
data = text;
}
}
const duration = performance.now() - startTime;
// Log response
if (shouldLog("INFO")) {
const level = response.status >= 500 ? "ERROR" : response.status >= 400 ? "WARN" : "INFO";
if (shouldLog(level)) {
console[level.toLowerCase()](
LOG_PREFIX,
`${method} ${url} — ${response.status} in ${formatDuration(duration)}`,
truncate(text || "", 200)
);
}
}
return {
response,
data,
meta: {
url,
method,
statusCode: response.status,
headers: response.headers,
},
};
} catch (err) {
const duration = performance.now() - startTime;
// Log error
if (shouldLog("ERROR")) {
console.error(
LOG_PREFIX,
`${method} ${url} — FAILED in ${formatDuration(duration)}`,
err?.message || err
);
}
throw err;
} finally {
clearTimeout(timeout);
}
}