/** * Shared SWR fetcher with standard error handling. */ export class FetchError extends Error { constructor( message: string, public status: number, ) { super(message); this.name = "FetchError"; } } export class ApiRequestError extends Error { constructor( message: string, public status: number, public payload?: unknown, ) { super(message); this.name = "ApiRequestError"; } } function extractErrorMessage(payload: unknown): string | null { if ( payload && typeof payload === "object" && "error" in payload && typeof payload.error === "string" ) { return payload.error; } if (typeof payload === "string" && payload.trim()) { return payload; } return null; } async function parsePayload(res: Response): Promise { if (res.status === 204) return undefined; if (typeof res.text === "function") { const text = await res.text(); if (!text) return undefined; try { return JSON.parse(text); } catch { return text; } } if (typeof res.json === "function") { try { return await res.json(); } catch { return undefined; } } return undefined; } type RequestJsonInit = Omit & { body?: TBody; }; /** * Shared JSON requester for imperative client-side API calls. * - Parses JSON/text payloads automatically * - Throws ApiRequestError with normalized message on non-2xx */ export async function requestJson( url: string, init: RequestJsonInit = {}, ): Promise { const { body, headers, ...rest } = init; const hasBody = body !== undefined; const mergedHeaders = new Headers(headers); if (hasBody && !mergedHeaders.has("Content-Type")) { mergedHeaders.set("Content-Type", "application/json"); } const res = await fetch(url, { ...rest, headers: mergedHeaders, body: hasBody ? JSON.stringify(body) : undefined, }); const payload = await parsePayload(res); if (!res.ok) { const message = extractErrorMessage(payload) ?? "请求失败"; throw new ApiRequestError(message, res.status, payload); } return payload as TResponse; } export async function fetcher(url: string): Promise { const res = await fetch(url); if (!res.ok) { throw new FetchError( res.status === 404 ? "NOT_FOUND" : res.status === 401 ? "UNAUTHORIZED" : "FETCH_ERROR", res.status, ); } return res.json(); }