227 lines
5.9 KiB
TypeScript
227 lines
5.9 KiB
TypeScript
/**
|
|
* 統一的API錯誤處理工具
|
|
* 提供一致的錯誤分類、格式化和處理邏輯
|
|
*/
|
|
|
|
export enum ApiErrorType {
|
|
NETWORK_ERROR = 'NETWORK_ERROR',
|
|
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
|
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
|
|
AUTHORIZATION_ERROR = 'AUTHORIZATION_ERROR',
|
|
NOT_FOUND_ERROR = 'NOT_FOUND_ERROR',
|
|
SERVER_ERROR = 'SERVER_ERROR',
|
|
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
|
|
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
|
|
}
|
|
|
|
export interface ApiError {
|
|
type: ApiErrorType
|
|
message: string
|
|
details?: any
|
|
statusCode?: number
|
|
timestamp: string
|
|
context?: string
|
|
}
|
|
|
|
export interface StandardApiResponse<T = any> {
|
|
success: boolean
|
|
data?: T
|
|
error?: ApiError
|
|
message?: string
|
|
}
|
|
|
|
/**
|
|
* 根據HTTP狀態碼確定錯誤類型
|
|
*/
|
|
const getErrorTypeFromStatus = (status: number): ApiErrorType => {
|
|
switch (true) {
|
|
case status === 400:
|
|
return ApiErrorType.VALIDATION_ERROR
|
|
case status === 401:
|
|
return ApiErrorType.AUTHENTICATION_ERROR
|
|
case status === 403:
|
|
return ApiErrorType.AUTHORIZATION_ERROR
|
|
case status === 404:
|
|
return ApiErrorType.NOT_FOUND_ERROR
|
|
case status >= 500:
|
|
return ApiErrorType.SERVER_ERROR
|
|
case status === 0:
|
|
return ApiErrorType.NETWORK_ERROR
|
|
default:
|
|
return ApiErrorType.UNKNOWN_ERROR
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 從Response對象創建統一的錯誤
|
|
*/
|
|
export const createApiErrorFromResponse = async (
|
|
response: Response,
|
|
context?: string
|
|
): Promise<ApiError> => {
|
|
let errorData: any = {}
|
|
|
|
try {
|
|
errorData = await response.json()
|
|
} catch {
|
|
errorData = { error: 'Failed to parse error response' }
|
|
}
|
|
|
|
const errorType = getErrorTypeFromStatus(response.status)
|
|
|
|
// 統一的錯誤訊息提取邏輯
|
|
let message = ''
|
|
if (errorData.error) {
|
|
message = errorData.error
|
|
} else if (errorData.message) {
|
|
message = errorData.message
|
|
} else if (errorData.title) {
|
|
message = errorData.title
|
|
} else if (errorData.details) {
|
|
message = errorData.details
|
|
} else {
|
|
message = getDefaultErrorMessage(errorType, response.status)
|
|
}
|
|
|
|
// 處理驗證錯誤的特殊格式
|
|
if (errorType === ApiErrorType.VALIDATION_ERROR && errorData.errors) {
|
|
const validationErrors = Object.values(errorData.errors).flat().join(', ')
|
|
message = validationErrors || message
|
|
}
|
|
|
|
return {
|
|
type: errorType,
|
|
message,
|
|
details: errorData,
|
|
statusCode: response.status,
|
|
timestamp: new Date().toISOString(),
|
|
context
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 從Exception創建統一的錯誤
|
|
*/
|
|
export const createApiErrorFromException = (
|
|
error: unknown,
|
|
context?: string
|
|
): ApiError => {
|
|
if (error instanceof Error) {
|
|
// 檢查是否為網路錯誤
|
|
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
return {
|
|
type: ApiErrorType.NETWORK_ERROR,
|
|
message: '網路連線失敗,請檢查網路連線',
|
|
timestamp: new Date().toISOString(),
|
|
context
|
|
}
|
|
}
|
|
|
|
// 檢查是否為超時錯誤
|
|
if (error.message.includes('timeout')) {
|
|
return {
|
|
type: ApiErrorType.TIMEOUT_ERROR,
|
|
message: '請求超時,請稍後重試',
|
|
timestamp: new Date().toISOString(),
|
|
context
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: ApiErrorType.UNKNOWN_ERROR,
|
|
message: error.message,
|
|
timestamp: new Date().toISOString(),
|
|
context
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: ApiErrorType.UNKNOWN_ERROR,
|
|
message: '發生未知錯誤',
|
|
timestamp: new Date().toISOString(),
|
|
context
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 獲取預設錯誤訊息
|
|
*/
|
|
const getDefaultErrorMessage = (type: ApiErrorType, statusCode?: number): string => {
|
|
switch (type) {
|
|
case ApiErrorType.NETWORK_ERROR:
|
|
return '網路連線失敗,請檢查網路連線'
|
|
case ApiErrorType.VALIDATION_ERROR:
|
|
return '輸入資料驗證失敗,請檢查輸入內容'
|
|
case ApiErrorType.AUTHENTICATION_ERROR:
|
|
return '身份驗證失敗,請重新登入'
|
|
case ApiErrorType.AUTHORIZATION_ERROR:
|
|
return '權限不足,無法執行此操作'
|
|
case ApiErrorType.NOT_FOUND_ERROR:
|
|
return '請求的資源不存在'
|
|
case ApiErrorType.SERVER_ERROR:
|
|
return '伺服器發生錯誤,請稍後重試'
|
|
case ApiErrorType.TIMEOUT_ERROR:
|
|
return '請求超時,請稍後重試'
|
|
default:
|
|
return `發生錯誤 (HTTP ${statusCode || 'Unknown'})`
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 創建成功的API回應
|
|
*/
|
|
export const createSuccessResponse = <T>(
|
|
data: T,
|
|
message?: string
|
|
): StandardApiResponse<T> => {
|
|
return {
|
|
success: true,
|
|
data,
|
|
message
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 創建錯誤的API回應
|
|
*/
|
|
export const createErrorResponse = (error: ApiError): StandardApiResponse => {
|
|
return {
|
|
success: false,
|
|
error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 檢查錯誤是否可重試
|
|
*/
|
|
export const isRetryableError = (error: ApiError): boolean => {
|
|
return [
|
|
ApiErrorType.NETWORK_ERROR,
|
|
ApiErrorType.TIMEOUT_ERROR,
|
|
ApiErrorType.SERVER_ERROR
|
|
].includes(error.type)
|
|
}
|
|
|
|
/**
|
|
* 獲取使用者友善的錯誤訊息
|
|
*/
|
|
export const getUserFriendlyErrorMessage = (error: ApiError): string => {
|
|
switch (error.type) {
|
|
case ApiErrorType.NETWORK_ERROR:
|
|
return '網路連線有問題,請檢查網路後重試'
|
|
case ApiErrorType.VALIDATION_ERROR:
|
|
return `輸入資料有誤:${error.message}`
|
|
case ApiErrorType.AUTHENTICATION_ERROR:
|
|
return '登入已過期,請重新登入'
|
|
case ApiErrorType.AUTHORIZATION_ERROR:
|
|
return '您沒有權限執行此操作'
|
|
case ApiErrorType.NOT_FOUND_ERROR:
|
|
return '找不到請求的內容'
|
|
case ApiErrorType.SERVER_ERROR:
|
|
return '伺服器暫時無法處理請求,請稍後重試'
|
|
case ApiErrorType.TIMEOUT_ERROR:
|
|
return '請求處理時間過長,請稍後重試'
|
|
default:
|
|
return error.message || '發生未知錯誤'
|
|
}
|
|
} |