diff --git a/app/[workspace]/project/[projectId]/(home)/product/page.tsx b/app/[workspace]/project/[projectId]/(home)/product/page.tsx index c5b9524d..9e76cc3b 100644 --- a/app/[workspace]/project/[projectId]/(home)/product/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/product/page.tsx @@ -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]);