m4: global API-error surface (no more silent failures)
The api client now catches network failures and non-JSON bodies robustly, and routes unexpected errors (offline / 5xx) to a global toast — 4xx stay with the caller so forms keep their inline messages. Toast actions are now optional (undo toasts keep their button; error toasts are message-only), and ToastHost moved from AppShell to the app root so toasts show everywhere, including the login screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -1,25 +1,52 @@
|
||||
import { useUiStore } from "../stores/ui";
|
||||
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
// Surface infra / unexpected failures (network, 5xx) globally. 4xx are left to
|
||||
// the caller — forms and views show their own inline messages for those.
|
||||
function notifyError(err: ApiError) {
|
||||
try {
|
||||
useUiStore().showToast(err.error);
|
||||
} catch {
|
||||
/* pinia not active yet (very early boot) — skip the toast */
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const resp = await fetch(path, {
|
||||
method,
|
||||
credentials: "include", // send/receive the signed session cookie
|
||||
headers: body !== undefined ? { "Content-Type": "application/json" } : undefined,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(path, {
|
||||
method,
|
||||
credentials: "include", // send/receive the signed session cookie
|
||||
headers: body !== undefined ? { "Content-Type": "application/json" } : undefined,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
} catch {
|
||||
const err: ApiError = { error: "Can't reach the server. Check your connection.", status: 0 };
|
||||
notifyError(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const text = await resp.text();
|
||||
const data: unknown = text ? JSON.parse(text) : {};
|
||||
let data: unknown = {};
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = {}; // non-JSON body (e.g. a proxy error page) — fall through to status handling
|
||||
}
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
const message =
|
||||
typeof data === "object" && data !== null && "error" in data
|
||||
? String((data as { error: unknown }).error)
|
||||
: "Request failed.";
|
||||
: "Something went wrong. Please try again.";
|
||||
const err: ApiError = { error: message, status: resp.status };
|
||||
if (resp.status >= 500) notifyError(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user