83 lines
4.0 KiB
TypeScript
83 lines
4.0 KiB
TypeScript
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
|
|
import { locale, t as translate } from '@@/i18n/runtime'
|
|
import { reportError } from '@@/utils/notifications'
|
|
import axios, { AxiosHeaders, CanceledError, isAxiosError, isCancel } from 'axios'
|
|
import { pinia } from '@/pinia'
|
|
import { useUserStore } from '@/pinia/stores/user'
|
|
|
|
export class ApiError extends Error {
|
|
constructor(readonly status: number, message: string, readonly data: Record<string, unknown> = {}, readonly code = status * 100) {
|
|
super(message)
|
|
}
|
|
}
|
|
interface RequestConfig extends Omit<AxiosRequestConfig<unknown>, 'auth'> {
|
|
anonymous?: boolean
|
|
errorOwner?: 'request' | 'caller'
|
|
/** 原文和下载需要 ETag、Content-Disposition 等响应头。 */
|
|
returnResponse?: boolean
|
|
}
|
|
const instance = axios.create({ baseURL: '/api/v1', adapter: 'fetch', withCredentials: false })
|
|
function envelope(value: unknown, status: number): ApiResponseData<unknown> {
|
|
if (!value || typeof value !== 'object' || !('code' in value) || typeof value.code !== 'number'
|
|
|| !Number.isInteger(value.code) || !('data' in value) || !('message' in value) || typeof value.message !== 'string') {
|
|
throw new ApiError(status, translate('common.invalid_server_response_protocol_194dc7'))
|
|
}
|
|
return value as ApiResponseData<unknown>
|
|
}
|
|
function errorData(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
}
|
|
async function failure(error: unknown): Promise<ApiError> {
|
|
if (!isAxiosError(error) || !error.response) return error instanceof ApiError ? error : new ApiError(0, error instanceof Error ? error.message : translate('common.request_failed_126fbf'))
|
|
const response = error.response
|
|
let body: unknown = response.data
|
|
if (body instanceof Blob) body = await body.text()
|
|
if (typeof body === 'string') {
|
|
try {
|
|
body = JSON.parse(body)
|
|
} catch {
|
|
return new ApiError(response.status, translate('common.request_failed_http_f21a09', { p0: response.status }))
|
|
}
|
|
}
|
|
try {
|
|
const data = envelope(body, response.status)
|
|
return new ApiError(response.status, data.message, errorData(data.data), data.code)
|
|
} catch (cause) {
|
|
if (cause instanceof ApiError) return cause
|
|
throw cause
|
|
}
|
|
}
|
|
export function request<T>(config: RequestConfig & { returnResponse: true }): Promise<AxiosResponse<T, unknown, AxiosHeaders>>
|
|
export function request<T>(config: RequestConfig): Promise<T>
|
|
export async function request<T>(config: RequestConfig): Promise<T | AxiosResponse<T, unknown, AxiosHeaders>> {
|
|
const user = useUserStore(pinia)
|
|
const version = user.getSessionVersion()
|
|
const token = config.anonymous === true ? '' : user.token
|
|
const headers = new AxiosHeaders(config.headers as ConstructorParameters<typeof AxiosHeaders>[0])
|
|
headers.set('Accept-Language', locale.value)
|
|
headers.set('Authorization', token ? `Bearer ${token}` : undefined)
|
|
try {
|
|
const response = await instance.request<T, AxiosResponse<T, unknown, AxiosHeaders>>({
|
|
...config,
|
|
headers,
|
|
signal: config.signal ? AbortSignal.any([user.getSessionSignal(), config.signal as AbortSignal]) : user.getSessionSignal(),
|
|
fetchOptions: { ...config.fetchOptions, credentials: 'omit', cache: 'no-store' },
|
|
})
|
|
user.assertSession(version)
|
|
if (response.status === 204) return config.returnResponse ? response : undefined as T
|
|
if (!config.responseType || config.responseType === 'json') {
|
|
const result = envelope(response.data, response.status)
|
|
if (result.code !== 0) throw new ApiError(response.status, result.message, errorData(result.data), result.code)
|
|
}
|
|
return config.returnResponse ? response : response.data
|
|
} catch (cause) {
|
|
if (isCancel(cause)) throw cause
|
|
if (version !== user.getSessionVersion()) throw new CanceledError(translate('common.session_changed_073f28'))
|
|
const error = await failure(cause)
|
|
user.assertSession(version)
|
|
if (error.status === 401 && token) user.invalidate(version)
|
|
if (config.errorOwner !== 'caller') reportError(error)
|
|
throw error
|
|
}
|
|
}
|