dramaling-vocab-learning/frontend/lib/api/errorHandler.ts

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 || '發生未知錯誤'
}
}