fix: admin health card no longer reports 'Network error' when API is reachable #4740
1 changed file+26−4
Modifiedlanding/src/components/Dashboard.tsx+26−4View fileUnifiedSplit
@@ -830,15 +830,37 @@ function DiagnosticsTab({ accessToken }: { accessToken: string }) {
830830 try {
831831 const r = await fetch(`${VOXLEN_BASE}/api/admin/health`, {
832832 headers: { Authorization: `Bearer ${accessToken}` },
833 signal: AbortSignal.timeout(6000),
833834 });
834 const json = (await r.json()) as HealthData & { error?: string };
835
836 // Read the body once as text, then parse defensively. A non-JSON payload
837 // (an HTML error page from a misconfigured deploy, an empty body, a proxy
838 // error) must NOT be reported as a network failure — the server clearly
839 // responded, so saying "is the API reachable?" contradicts the endpoint
840 // pings, which already show it answering in ~40ms.
841 const raw = await r.text();
842 let json: (HealthData & { error?: string }) | null = null;
843 try {
844 json = raw ? (JSON.parse(raw) as HealthData & { error?: string }) : null;
845 } catch {
846 json = null;
847 }
848
835849 if (!r.ok) {
836 setHealthError(json.error ?? "Failed");
850 setHealthError(json?.error ?? `HTTP ${r.status} ${r.statusText}`.trim());
851 } else if (!json) {
852 setHealthError(`Reachable, but returned a non-JSON response (HTTP ${r.status}).`);
837853 } else {
838854 setHealth(json);
839855 }
840 } catch {
841 setHealthError("Network error — is the API reachable?");
856 } catch (err) {
857 // Only a thrown fetch is a genuine transport failure. Distinguish a
858 // timeout from an unreachable host so the banner matches reality.
859 setHealthError(
860 err instanceof DOMException && err.name === "TimeoutError"
861 ? "Request timed out — the API did not respond within 6s."
862 : "Network error — is the API reachable?"
863 );
842864 }
843865 setHealthLoading(false);
844866 }, [accessToken]);
845867
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts