fix(project): surface API errors instead of hanging on Loading…

Adds a 10s timeout + AbortController to the codebases fetch and
parses error bodies defensively (so a non-JSON 4xx/5xx still shows
a status code instead of disappearing). The Product tab will now
show a clear message ("Request timed out", "HTTP 401 Unauthorized",
etc.) instead of spinning forever when the API can't respond.

Made-with: Cursor
This commit is contained in:
2026-04-28 17:17:15 -07:00
parent 56d4cc36c7
commit b9adcb76b6

View File

@@ -46,27 +46,50 @@ export default function ProductTab() {
setReason(undefined);
setSelectedFile(null);
fetch(`/api/projects/${projectId}/codebases`, { credentials: "include" })
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
fetch(`/api/projects/${projectId}/codebases`, {
credentials: "include",
signal: controller.signal,
})
.then(async r => {
const data = (await r.json()) as CodebasesResponse;
if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
return data;
let body: CodebasesResponse | { error?: string } = {};
try {
body = await r.json();
} catch {
/* non-JSON body — fall through to status-only error */
}
if (!r.ok) {
const msg = (body as { error?: string }).error || `HTTP ${r.status} ${r.statusText}`.trim();
throw new Error(msg);
}
return body as CodebasesResponse;
})
.then(data => {
if (cancelled) return;
setCodebases(data.codebases);
setCodebases(data.codebases ?? []);
setReason(data.reason);
// Auto-expand the first codebase so users see something
if (data.codebases[0]) {
if (data.codebases?.[0]) {
setExpanded(new Set([data.codebases[0].id]));
}
})
.catch(err => {
if (!cancelled) setListError(err.message || "Failed to load");
if (cancelled) return;
if (err?.name === "AbortError") {
setListError("Request timed out after 10s.");
} else {
setListError(err?.message || "Failed to load codebases");
}
})
.finally(() => {
clearTimeout(timeout);
});
return () => {
cancelled = true;
controller.abort();
clearTimeout(timeout);
};
}, [projectId]);