feat(web): fetch-based api client with 401 handling and tests

This commit is contained in:
2026-04-29 07:49:33 +08:00
parent 7c164f6aea
commit 3eb595cf5e
6 changed files with 181 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import { ApiException, type ApiError } from './types'
const BASE = '' // 同源;dev 走 vite proxy
interface RequestOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'
body?: unknown
signal?: AbortSignal
}
let authTokenProvider: () => string | null = () => null
let onUnauthorized: () => void = () => {}
export function configure(opts: {
tokenProvider: () => string | null
onUnauthorized: () => void
}) {
authTokenProvider = opts.tokenProvider
onUnauthorized = opts.onUnauthorized
}
export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-Request-ID': crypto.randomUUID(),
}
const token = authTokenProvider()
if (token) headers['Authorization'] = `Bearer ${token}`
const resp = await fetch(BASE + path, {
method: opts.method || 'GET',
headers,
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
signal: opts.signal,
})
if (resp.status === 401) {
onUnauthorized()
}
if (resp.status === 204) {
return undefined as T
}
const text = await resp.text()
const data = text ? JSON.parse(text) : null
if (!resp.ok) {
throw new ApiException(
resp.status,
(data as ApiError) || { code: 'unknown', message: resp.statusText },
)
}
return data as T
}