124 lines
3.1 KiB
TypeScript
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 })
|
|
}
|