Fix icon-only error toasts: never throw ApiError with an empty message

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 <status>)" message instead. Same fix in
the UploadTemplateDialog's raw-fetch path and in the web-demo copies.
This commit is contained in:
Anand
2026-09-14 01:13:35 +05:30
parent d9b2519b75
commit 52424fb3c8
4 changed files with 32 additions and 4 deletions
@@ -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
}
+9 -1
View File
@@ -55,7 +55,15 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
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
}
@@ -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
}
+9 -1
View File
@@ -63,7 +63,15 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
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
}