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 }