expo-popcore-app/lib/fetch-logger.ts

164 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}