Files
skt-vuetify-templates/src/services/error.ts
T
skytek_xinliang 71683482e1 refactor: ky
2026-05-07 11:17:30 +08:00

124 lines
3.1 KiB
TypeScript

import type { ApiError } from '@/types/api'
import { isHTTPError, isTimeoutError } from 'ky'
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function firstString(value: unknown): string | undefined {
if (typeof value === 'string') {
const trimmed = value.trim()
return trimmed ? trimmed : undefined
}
if (Array.isArray(value)) {
for (const item of value) {
const found = firstString(item)
if (found) return found
}
}
if (isRecord(value)) {
const message = firstString(value.message)
if (message) return message
// 常見錯誤格式:{ error: '...' } / { error: { message: '...' } }
const errorValue = value.error
const errorMessage = firstString(errorValue)
if (errorMessage) return errorMessage
// RFC 7807 (problem+json): { title, detail }
const detail = firstString(value.detail)
if (detail) return detail
const title = firstString(value.title)
if (title) return title
// 有些後端用 msg
const msg = firstString(value.msg)
if (msg) return msg
// errors: Record<string, string[]>
const errors = value.errors
if (isRecord(errors)) {
for (const key of Object.keys(errors)) {
const found = firstString(errors[key])
if (found) return found
}
}
}
return undefined
}
export function extractErrorMessage(data: unknown): string | undefined {
return firstString(data)
}
export class ApiRequestError extends Error {
code?: number
status?: number
errors?: ApiError['errors']
raw?: unknown
constructor(params: {
message: string
code?: number
status?: number
errors?: ApiError['errors']
raw?: unknown
}) {
super(params.message)
this.name = 'ApiRequestError'
this.code = params.code
this.status = params.status
this.errors = params.errors
this.raw = params.raw
}
}
export class CanceledRequestError extends ApiRequestError {
constructor() {
super({ message: '請求已取消' })
this.name = 'CanceledRequestError'
}
}
export function isRequestCanceled(error: unknown): boolean {
return error instanceof DOMException && error.name === 'AbortError'
}
export function normalizeError(error: unknown): ApiRequestError {
if (error instanceof ApiRequestError) {
return error
}
if (isRequestCanceled(error)) {
return new CanceledRequestError()
}
if (isHTTPError(error)) {
const status = error.response.status
const data = error.data
const message = extractErrorMessage(data) || error.message || '請求失敗'
const apiError = isRecord(data) ? (data as Partial<ApiError>) : undefined
const code = apiError?.code ?? status
return new ApiRequestError({
message,
code,
status,
errors: apiError?.errors,
raw: error,
})
}
if (isTimeoutError(error)) {
return new ApiRequestError({
message: '請求逾時',
raw: error,
})
}
if (error instanceof Error) {
return new ApiRequestError({ message: error.message, raw: error })
}
return new ApiRequestError({ message: '未知錯誤', raw: error })
}