164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
import { router } from 'expo-router';
|
||
import Toast from '@/components/ui/Toast';
|
||
|
||
import { storage } from './storage';
|
||
|
||
// 统一的 Token 存储键名(与 lib/auth.ts 保持一致)
|
||
export const TOKEN_KEY = 'bestaibest.better-auth.session_token';
|
||
|
||
interface FetchLoggerOptions {
|
||
enableLogging?: boolean
|
||
logRequest?: boolean
|
||
logResponse?: boolean
|
||
logError?: boolean
|
||
}
|
||
|
||
const defaultOptions: FetchLoggerOptions = {
|
||
enableLogging: __DEV__,
|
||
logRequest: true,
|
||
logResponse: true,
|
||
logError: true,
|
||
}
|
||
|
||
const originalFetch = global.fetch
|
||
|
||
// 防止重复跳转的标志
|
||
let isRedirecting = false
|
||
|
||
export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => {
|
||
const config = { ...defaultOptions, ...options }
|
||
|
||
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||
const startTime = Date.now()
|
||
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
|
||
const method = init?.method || 'GET'
|
||
|
||
try {
|
||
// 使用统一的 Token 存储键
|
||
const token = await storage.getItem(TOKEN_KEY)
|
||
|
||
// 确保 headers 是一个对象
|
||
const headers: Record<string, string> = {}
|
||
if (init?.headers) {
|
||
if (init.headers instanceof Headers) {
|
||
init.headers.forEach((value, key) => {
|
||
headers[key] = value
|
||
})
|
||
} else if (Array.isArray(init.headers)) {
|
||
init.headers.forEach(([key, value]) => {
|
||
headers[key] = value as string
|
||
})
|
||
} else {
|
||
Object.assign(headers, init.headers)
|
||
}
|
||
}
|
||
|
||
// 添加 Bearer Token
|
||
if (token) {
|
||
headers['authorization'] = `Bearer ${token}`
|
||
}
|
||
|
||
// 确保 Content-Type 正确设置(FormData 不需要手动设置,让浏览器自动处理 boundary)
|
||
if (method !== 'GET' && method !== 'HEAD' && !headers['Content-Type'] && !(init?.body instanceof FormData)) {
|
||
headers['Content-Type'] = 'application/json'
|
||
}
|
||
|
||
// 序列化请求体(如果是对象)
|
||
let body = init?.body
|
||
if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)) {
|
||
body = JSON.stringify(body)
|
||
}
|
||
|
||
const response = await originalFetch(input, {
|
||
...init,
|
||
headers,
|
||
...(body !== undefined && { body }),
|
||
})
|
||
|
||
// 401 未授权处理
|
||
if (response.status === 401 && !isRedirecting) {
|
||
// 检查是否是登录/注册接口的 401 错误(用户名密码错误)
|
||
const isAuthEndpoint = url.includes('/sign-in/') || url.includes('/sign-up/')
|
||
|
||
// 如果是认证接口的 401,不处理,让错误传递到组件
|
||
if (isAuthEndpoint) {
|
||
console.log('🔐 认证接口返回 401,不处理')
|
||
return response
|
||
}
|
||
|
||
// 其他接口的 401 才认为是 Token 过期
|
||
console.warn('🔐 401 未授权,跳转到登录页')
|
||
isRedirecting = true
|
||
|
||
// 显示 Toast 提示(安全检查)
|
||
try {
|
||
Toast.show('登录已过期,请重新登录')
|
||
} catch (error) {
|
||
console.warn('[Toast] 显示失败:', error)
|
||
}
|
||
|
||
// 清除过期 Token
|
||
await storage.removeItem(TOKEN_KEY)
|
||
|
||
// 跳转到登录页(使用 replace 避免返回栈问题)
|
||
router.replace('/auth')
|
||
|
||
// 延迟重置标志(防止并发请求导致重复跳转)
|
||
setTimeout(() => {
|
||
isRedirecting = false
|
||
}, 1000)
|
||
}
|
||
|
||
return response
|
||
} catch (error) {
|
||
const duration = Date.now() - startTime
|
||
|
||
if (config.enableLogging && config.logError) {
|
||
console.group(`❌ [FETCH 错误] ${method} ${url}`)
|
||
console.error('📍 URL:', url)
|
||
console.error('🔧 Method:', method)
|
||
console.error('⏱️ Duration:', `${duration}ms`)
|
||
|
||
if (init?.headers) {
|
||
console.error('📋 Request Headers:', JSON.stringify(init.headers, null, 2))
|
||
}
|
||
|
||
if (init?.body) {
|
||
try {
|
||
const bodyContent = typeof init.body === 'string' ? init.body : JSON.stringify(init.body)
|
||
console.error('📦 Request Body:', bodyContent)
|
||
} catch (e) {
|
||
console.error('📦 Request Body: [无法序列化]')
|
||
}
|
||
}
|
||
|
||
console.error('💥 Error Type:', error?.constructor?.name || 'Unknown')
|
||
console.error('💥 Error Message:', error instanceof Error ? error.message : String(error))
|
||
|
||
if (error instanceof Error && error.stack) {
|
||
console.error('📚 Stack Trace:', error.stack)
|
||
}
|
||
|
||
if (error && typeof error === 'object') {
|
||
console.error('🔍 Error Details:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2))
|
||
}
|
||
|
||
console.groupEnd()
|
||
}
|
||
|
||
throw error
|
||
}
|
||
}
|
||
}
|
||
|
||
export const fetchWithLogger = createFetchWithLogger()
|
||
|
||
export const setupGlobalFetchLogger = (options?: FetchLoggerOptions) => {
|
||
const loggedFetch = createFetchWithLogger(options)
|
||
global.fetch = loggedFetch as typeof fetch
|
||
|
||
return () => {
|
||
global.fetch = originalFetch
|
||
}
|
||
}
|