Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>("/me")
.then((next) => { setMe(next); setMeError("") })
.catch((e: Error) => setMeError(e.message || "网络错误"))
.catch((e: Error) => setMeError(e.message))
}, [])

useEffect(() => {
Expand Down
5 changes: 1 addition & 4 deletions src/components/NodeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,7 @@ function useHistory(id: number, hours: number, series: "metrics" | "ping") {
api<History>(`/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])
Expand Down
32 changes: 27 additions & 5 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiError> {
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<T>(path: string, init?: RequestInit): Promise<T> {
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. */
Expand Down
Loading