From b9adcb76b603c82234d736a64b6bc915da3d1d53 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Tue, 28 Apr 2026 17:17:15 -0700 Subject: [PATCH] =?UTF-8?q?fix(project):=20surface=20API=20errors=20instea?= =?UTF-8?q?d=20of=20hanging=20on=20Loading=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../[projectId]/(home)/product/page.tsx | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) 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]);