From dccd5eba6716461d1b311523dccf1a3ac8fc3d37 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Fri, 25 Sep 2026 02:44:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8E=A5=E5=8F=A3=E5=87=BA=E9=94=99?= =?UTF-8?q?=E6=97=B6=E5=8F=AA=E6=98=BE=E7=A4=BA=20hub=20=E7=9A=84=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=96=87=E6=A1=88=EF=BC=8C=E5=8F=8D=E4=BB=A3=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E9=A1=B5=E4=B8=8E=E6=96=AD=E7=BD=91=E6=8C=89=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不是 text/plain 的错误回复来自反代或 CDN,不再把它的 HTML 显示给访客;断网、 200 却不是 JSON 也给出中文说明。错误信息不会再是空串,去掉两处兜底。 --- src/App.tsx | 4 +--- src/components/NodeDetail.tsx | 5 +---- src/lib/api.ts | 32 +++++++++++++++++++++++++++----- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 84590a2..038a6dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -98,11 +98,9 @@ export default function App() { const open = useNodeRoute() const loadMe = useCallback(() => { - // `|| "..."`: HTTP/2 has no statusText, so a bodiless 502 from a proxy arrives - // as "" and would otherwise render as still loading, with no retry button. return api("/me") .then((next) => { setMe(next); setMeError("") }) - .catch((e: Error) => setMeError(e.message || "网络错误")) + .catch((e: Error) => setMeError(e.message)) }, []) useEffect(() => { diff --git a/src/components/NodeDetail.tsx b/src/components/NodeDetail.tsx index fe602bc..59fe26d 100644 --- a/src/components/NodeDetail.tsx +++ b/src/components/NodeDetail.tsx @@ -137,10 +137,7 @@ function useHistory(id: number, hours: number, series: "metrics" | "ping") { api(`/nodes/${id}/metrics?hours=${hours}&points=${points}&series=${series}`) .then((next) => { if (active) setData(next) }) .catch((e: Error) => { - // `|| "..."` as in App.tsx: HTTP/2 dropped statusText, so a bodiless - // failure from a proxy arrives as the empty string and renders as no - // error. - if (active) { setFailed(e.message || "网络错误"); setData({ metrics: [], ping: [], probes: {} }) } + if (active) { setFailed(e.message); setData({ metrics: [], ping: [], probes: {} }) } }) return () => { active = false } }, [id, hours, series, attempt]) diff --git a/src/lib/api.ts b/src/lib/api.ts index 66f5170..bfcd232 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -83,13 +83,35 @@ class ApiError extends Error { } } +/** + * Every error the hub answers is one line of plain text written for the reader. + * Anything else came from something in front of it -- a proxy's error page, a + * CDN's challenge, an empty 502 -- and is described by its status instead. + */ +async function failure(res: Response): Promise { + const text = res.headers.get("content-type")?.startsWith("text/plain") ? (await res.text()).trim() : "" + return new ApiError( + res.status, + text || (res.status >= 500 ? `服务暂时无法访问(HTTP ${res.status}),稍后再试` : `请求被拦截(HTTP ${res.status}),稍后再试`), + ) +} + export async function api(path: string, init?: RequestInit): Promise { - const res = await fetch(`/api${path}`, { - ...init, - headers: init?.body ? { "content-type": "application/json", ...init?.headers } : init?.headers, + let res: Response + try { + res = await fetch(`/api${path}`, { + ...init, + headers: init?.body ? { "content-type": "application/json", ...init?.headers } : init?.headers, + }) + } catch { + throw new ApiError(0, "网络连接失败,稍后再试") + } + if (!res.ok) throw await failure(res) + if (res.status === 204) return undefined as T + // A 200 carrying HTML is a proxy's page, not the hub's JSON. + return res.json().catch(() => { + throw new ApiError(res.status, "收到的不是状态数据,稍后再试") }) - if (!res.ok) throw new ApiError(res.status, (await res.text()) || res.statusText) - return res.status === 204 ? (undefined as T) : res.json() } /** A malformed report must not remove every other node from the page. */