summaryrefslogtreecommitdiff
path: root/frontend/src/lib/api.ts
blob: 923a6e85f66c3fdcf1f2f52b36808e439c509449 (plain)
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;
}