108 lines
2.5 KiB
TypeScript
108 lines
2.5 KiB
TypeScript
/**
|
|
* 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<unknown> {
|
|
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<TBody> = Omit<RequestInit, "body"> & {
|
|
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<TResponse = unknown, TBody = unknown>(
|
|
url: string,
|
|
init: RequestJsonInit<TBody> = {},
|
|
): Promise<TResponse> {
|
|
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<T = unknown>(url: string): Promise<T> {
|
|
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();
|
|
}
|