1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
export interface ApiErrorShape {
code: string;
message: string;
details?: unknown;
}
export interface ApiErrorBody {
error: ApiErrorShape;
}
export class ApiError extends Error {
status: number;
code: string;
details?: unknown;
constructor(status: number, code: string, message: string, details?: unknown) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
this.details = details;
}
}
interface Options {
method?: string;
body?: unknown;
signal?: AbortSignal;
}
export async function apiFetch<T = unknown, R = T>(url: string, options: Options = {}): Promise<R> {
const headers: Record<string, string> = { accept: 'application/json' };
let bodyStr: string | undefined;
if (options.body !== undefined) {
headers['content-type'] = 'application/json';
bodyStr = JSON.stringify(options.body);
}
let res: Response;
try {
res = await fetch(url, {
method: options.method ?? 'GET',
headers,
body: bodyStr,
credentials: 'same-origin',
signal: options.signal
});
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') throw e;
throw new ApiError(0, 'network', '网络请求失败,请确认服务已启动后重试');
}
if (res.status === 204) {
return undefined as R;
}
const contentType = res.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) {
if (!res.ok) {
throw new ApiError(res.status, 'server_error', `服务返回错误(HTTP ${res.status})`);
}
throw new ApiError(res.status, 'bad_response', '服务返回了非预期的非 JSON 响应(接口约定为 JSON)');
}
const parsed: unknown = await res.json().catch(() => null);
if (!res.ok) {
const errBody = parsed as ApiErrorBody | null;
if (errBody?.error) {
throw new ApiError(res.status, errBody.error.code, errBody.error.message, errBody.error.details);
}
throw new ApiError(res.status, 'server_error', `服务返回错误(HTTP ${res.status})`);
}
return parsed as R;
}
|