Files
thoughtsync/frontend/src/api/client.ts
T
bvandeusenandClaude Opus 4.8 e8e0d86413
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 31s
M9 S2: fold raw multipart fetches into api.postForm
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
2026-07-23 21:18:37 -04:00

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),
};