apoorvrajdev's picture
feat(frontend): complete Phase 2B frontend-backend integration
a8f12e6
const DEFAULT_API_BASE = "http://127.0.0.1:8000";
const HEALTH_TIMEOUT_MS = 3000;
const CAPTION_TIMEOUT_MS = 60000;
export const API_BASE = (
import.meta.env?.VITE_API_BASE ?? DEFAULT_API_BASE
).replace(/\/$/, "");
class ApiError extends Error {
constructor(message, { kind = "unknown", status = null, cause } = {}) {
super(message);
this.name = "ApiError";
this.kind = kind;
this.status = status;
if (cause) this.cause = cause;
}
}
const isAbortError = (err) => err?.name === "AbortError" || err?.code === 20;
const classifyFetchError = (err) => {
if (isAbortError(err)) {
return new ApiError("Request timed out.", { kind: "timeout", cause: err });
}
// Browsers surface CORS denials and network failures as a generic TypeError.
if (err instanceof TypeError) {
return new ApiError(
"Cannot reach backend. Check that the API is running and CORS allows this origin.",
{ kind: "network", cause: err },
);
}
return new ApiError(err?.message || "Request failed.", {
kind: "unknown",
cause: err,
});
};
const fetchWithTimeout = async (url, { timeoutMs, ...init } = {}) => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
};
export const checkHealth = async () => {
try {
const response = await fetchWithTimeout(`${API_BASE}/healthz`, {
timeoutMs: HEALTH_TIMEOUT_MS,
headers: { Accept: "application/json" },
});
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
};
export const captionImage = async (imageFile) => {
const formData = new FormData();
formData.append("image", imageFile);
let response;
try {
response = await fetchWithTimeout(`${API_BASE}/v1/captions`, {
method: "POST",
body: formData,
timeoutMs: CAPTION_TIMEOUT_MS,
});
} catch (err) {
throw classifyFetchError(err);
}
if (!response.ok) {
let detail = `HTTP ${response.status}`;
try {
const errorData = await response.json();
if (errorData?.detail) detail = errorData.detail;
} catch {
/* response body was not JSON */
}
throw new ApiError(detail, { kind: "http", status: response.status });
}
return response.json();
};
export { ApiError };