import type { ApiError } from '@/types/api' import { isHTTPError, isTimeoutError } from 'ky' function isRecord(value: unknown): value is Record { 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 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) : 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 }) }