import { type AxiosError, AxiosHeaders, type AxiosInstance } from 'axios' import { extractErrorMessage, normalizeError } from './error' import { emitHttpError } from './http-error' import { emitHttpToast } from './http-toast' import { emitForceLogout } from './session' import { tokenService } from './token' // Axios 攔截器 // // 設計重點: // - Request:自動注入 token(從 localStorage 讀取) // - 使用 tokenService 作為單一來源,避免 interceptor 直接 import Pinia store 造成循環依賴 // store(auth) -> services(userApi) -> httpClient -> interceptors -> store(auth) // - Response:統一處理 HTTP 錯誤(目前示範 401/403/500) // - 使用 normalizeError 將錯誤轉成 ApiRequestError // // 注意: // - Store 仍然是唯一負責「寫入/清除 token」的地方(login/logout) // - Interceptor 只負責「讀取 token 並附加到 request」 export function setupInterceptors (client: AxiosInstance) { // Request: 自動注入 token client.interceptors.request.use( (config) => { const token = tokenService.getToken() const url = config.url ?? '' const shouldAttachToken = !url.includes('/Auth/login') if (token && shouldAttachToken) { const headers = AxiosHeaders.from(config.headers ?? {}) headers.set('Authorization', `Bearer ${token}`) config.headers = headers } return config }, (error) => { return Promise.reject(error) } ) // Response: 統一錯誤處理 client.interceptors.response.use( (response) => response, (error: AxiosError) => { const normalized = normalizeError(error) // 取消請求不做全域錯誤導頁 if (error.code === 'ERR_CANCELED') { return Promise.reject(normalized) } const requestUrl = error.config?.url ?? '' const isLoginRequest = requestUrl.includes('/Auth/login') const silentToast = Boolean(error.config?.meta?.silentToast) // 統一處理 HTTP 狀態碼錯誤 const status = error.response?.status switch (status) { // 401 Unauthorized case 401: { // 不是所有 401 都代表「token 過期」: // - 登入失敗通常也會是 401,但不應觸發全域登出流程 // 這裡以「是否帶 Authorization header」作為判斷依據 { const url = error.config?.url ?? '' if (url.includes('/Auth/login')) break const requestHeaders = AxiosHeaders.from(error.config?.headers ?? {}) const hasAuthHeader = Boolean(requestHeaders.get('Authorization')) if (hasAuthHeader) { tokenService.clearToken() const backendMessage = extractErrorMessage(error.response?.data) emitForceLogout({ message: backendMessage }) } } break } // 403 Forbidden case 403: { emitHttpError({ status, message: normalized.message }) break } // 404 Not Found(API 端點不存在/被移除) case 404: { emitHttpError({ status, message: normalized.message }) break } // 500 Internal Server Error case 500: { // 500 通常是「單一 API 失敗」:交由呼叫端決定 UI(snackbar/區塊錯誤/重試) // 避免同一頁多支 API 時,其中一支 500 就把整個頁面導走 break } // 503 Service Unavailable case 503: { // 503 通常對使用者來說就是「系統維護/暫時無法使用」 emitHttpError({ status, message: normalized.message }) break } default: // 無 response status 時,多半是網路/跨網域/連線問題: // 交由呼叫端決定 UI(snackbar/區塊錯誤/重試),避免全域導頁打斷使用者操作 } const shouldToast = !silentToast && !isLoginRequest && status !== 401 && status !== 403 && status !== 404 && status !== 503 && (status === 500 || !status || (typeof status === 'number' && status >= 500)) if (shouldToast) { emitHttpToast({ level: status ? 'error' : 'warning', message: normalized.message }) } return Promise.reject(normalized) } ) }