Both notes-store uploads (uploadAttachment, importNotes) hand-rolled the same fetch + resp.json() + !ok error parsing that api.client already does. Add `api.postForm<T>(path, form)`: request() now detects a FormData body and lets the browser set the multipart Content-Type (skipping the JSON header + stringify), reusing the shared error handling — so the two uploads gain network-error handling and the 5xx infra toast they lacked. A too-large import returns 413 (< 500), so it still throws for inline display rather than toasting. DRY: net -13 lines; no raw fetch() remains in the stores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
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> {
|
|
// FormData carries its own multipart Content-Type (with boundary), so let the
|
|
// browser set it — only JSON bodies get an explicit header + stringify.
|
|
const isForm = body instanceof FormData;
|
|
let resp: Response;
|
|
try {
|
|
resp = await fetch(path, {
|
|
method,
|
|
credentials: "include", // send/receive the signed session cookie
|
|
headers: body !== undefined && !isForm ? { "Content-Type": "application/json" } : undefined,
|
|
body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
|
|
});
|
|
} catch {
|
|
const err: ApiError = { error: "Can't reach the server. Check your connection.", status: 0 };
|
|
notifyError(err);
|
|
throw err;
|
|
}
|
|
|
|
const text = await resp.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)
|
|
: "Something went wrong. Please try again.";
|
|
const err: ApiError = { error: message, status: resp.status };
|
|
if (resp.status >= 500) notifyError(err);
|
|
throw err;
|
|
}
|
|
|
|
return data as T;
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string) => request<T>("GET", path),
|
|
post: <T>(path: string, body?: unknown) => request<T>("POST", path, body),
|
|
patch: <T>(path: string, body?: unknown) => request<T>("PATCH", path, body),
|
|
put: <T>(path: string, body?: unknown) => request<T>("PUT", path, body),
|
|
del: <T>(path: string) => request<T>("DELETE", path),
|
|
postForm: <T>(path: string, form: FormData) => request<T>("POST", path, form),
|
|
};
|