From 52424fb3c8f08ec2e55e2a8f746762ebea9cd65c Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 14 Sep 2026 01:13:35 +0530 Subject: [PATCH] Fix icon-only error toasts: never throw ApiError with an empty message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Error responses that aren't the backend's {"error": ...} JSON shape (reverse-proxy 502/504 HTML pages, non-JSON bodies) fell back to res.statusText, which is an empty string over HTTP/2 — so every toast.error(err.message) call rendered a toast with an icon and no text, in light and dark themes alike. Fall through empty/non-string values to a generic "Request failed (HTTP )" message instead. Same fix in the UploadTemplateDialog's raw-fetch path and in the web-demo copies. --- .../src/components/inventory/UploadTemplateDialog.tsx | 8 +++++++- web-demo/src/lib/api.ts | 10 +++++++++- web/src/components/inventory/UploadTemplateDialog.tsx | 8 +++++++- web/src/lib/api.ts | 10 +++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/web-demo/src/components/inventory/UploadTemplateDialog.tsx b/web-demo/src/components/inventory/UploadTemplateDialog.tsx index 43ff3a0..bb00dd5 100644 --- a/web-demo/src/components/inventory/UploadTemplateDialog.tsx +++ b/web-demo/src/components/inventory/UploadTemplateDialog.tsx @@ -17,7 +17,13 @@ import { api, ApiError, type ClusterResource, type Storage } from "@/lib/api" async function uploadFile(path: string, form: FormData): Promise<{ upid: string }> { const res = await fetch(`/api/v1${path}`, { method: "POST", body: form, credentials: "include" }) const data = await res.json().catch(() => undefined) - if (!res.ok) throw new ApiError(res.status, data?.error ?? res.statusText, data?.code) + if (!res.ok) { + throw new ApiError( + res.status, + (typeof data?.error === "string" && data.error) || res.statusText || `Request failed (HTTP ${res.status})`, + data?.code, + ) + } return data } diff --git a/web-demo/src/lib/api.ts b/web-demo/src/lib/api.ts index dc970e7..fc22442 100644 --- a/web-demo/src/lib/api.ts +++ b/web-demo/src/lib/api.ts @@ -55,7 +55,15 @@ async function request(path: string, init?: RequestInit): Promise { if (data?.code) { for (const handler of errorCodeHandlers) handler(data.code) } - throw new ApiError(res.status, data?.error ?? res.statusText, data?.code) + // `||` (not ??) so empty values fall through: statusText is "" over + // HTTP/2, and an empty message renders as an icon-only toast. The typeof + // guard keeps a non-string `error` (e.g. an OpenAI-style object from a + // proxy) from becoming "[object Object]". + throw new ApiError( + res.status, + (typeof data?.error === "string" && data.error) || res.statusText || `Request failed (HTTP ${res.status})`, + data?.code, + ) } return data as T } diff --git a/web/src/components/inventory/UploadTemplateDialog.tsx b/web/src/components/inventory/UploadTemplateDialog.tsx index fb066a0..7af0506 100644 --- a/web/src/components/inventory/UploadTemplateDialog.tsx +++ b/web/src/components/inventory/UploadTemplateDialog.tsx @@ -17,7 +17,13 @@ import { api, ApiError, type ClusterResource, type Storage } from "@/lib/api" async function uploadFile(path: string, form: FormData): Promise<{ upid: string }> { const res = await fetch(`/api/v1${path}`, { method: "POST", body: form, credentials: "include" }) const data = await res.json().catch(() => undefined) - if (!res.ok) throw new ApiError(res.status, data?.error ?? res.statusText, data?.code) + if (!res.ok) { + throw new ApiError( + res.status, + (typeof data?.error === "string" && data.error) || res.statusText || `Request failed (HTTP ${res.status})`, + data?.code, + ) + } return data } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 13dcd5c..113d981 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -63,7 +63,15 @@ async function request(path: string, init?: RequestInit): Promise { if (data?.code) { for (const handler of errorCodeHandlers) handler(data.code) } - throw new ApiError(res.status, data?.error ?? res.statusText, data?.code) + // `||` (not ??) so empty values fall through: statusText is "" over + // HTTP/2, and an empty message renders as an icon-only toast. The typeof + // guard keeps a non-string `error` (e.g. an OpenAI-style object from a + // proxy) from becoming "[object Object]". + throw new ApiError( + res.status, + (typeof data?.error === "string" && data.error) || res.statusText || `Request failed (HTTP ${res.status})`, + data?.code, + ) } return data as T }