import axios from 'axios'; import { message } from 'antd'; import { getRequestErrorMessage, isAuthSessionFailure } from './authSessionPolicy'; import { requestDesktopSessionRenewal } from '../utils/desktopEmbedSession'; const DEFAULT_REQUEST_TIMEOUT_MS = Math.max( Number(import.meta.env.VITE_API_REQUEST_TIMEOUT_MS) || 120000, 15000 ); const request = axios.create({ baseURL: '', // Proxy handles the base URL timeout: DEFAULT_REQUEST_TIMEOUT_MS, }); const AUTH_USER_STORAGE_KEY = 'auth_user'; // 后端重启窗口内的自动重连 // ------------------------------------------------------------------ // 本地/线上后端在「隧道掉线 -> 监督器重启后端」期间会有 20~90 秒不可用, // 此时 Vite 代理拿不到后端,会直接回 500(响应体是 HTML,没有 error 字段), // 用户看到的就是一句没头没尾的 "Request failed with status code 500"。 // 这里对**幂等请求**(GET/HEAD/OPTIONS)在网络错误或 5xx 时做退避重试, // 重启窗口过去后请求会自己成功,页面不再报错。 // 累计等待约 80 秒,覆盖最坏情况下的后端启动耗时。 const RETRY_BACKOFF_MS = [1500, 3000, 5000, 8000, 13000, 21000, 30000]; const RETRYABLE_METHODS = ['get', 'head', 'options']; const RETRY_NOTICE_KEY = 'api-reconnect-notice'; function isRetryableRequest(config) { if (!config || config.retry === false) { return false; } const method = String(config.method || 'get').toLowerCase(); return RETRYABLE_METHODS.includes(method); } function isRetryableError(error) { if (axios.isCancel?.(error) || error?.code === 'ERR_CANCELED') { return false; } const status = error?.response?.status; // 无响应:网络中断、连接被拒(后端还没起来)、代理转发失败 if (!status) { return true; } // 5xx:后端内部错误 / 网关与代理错误 return status >= 500 && status <= 599; } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function notifyReconnecting() { message.open({ key: RETRY_NOTICE_KEY, type: 'loading', content: '服务正在重启,正在自动重连…', duration: 0, }); } function clearReconnectNotice() { message.destroy(RETRY_NOTICE_KEY); } function clearAuthSession() { localStorage.removeItem('token'); localStorage.removeItem(AUTH_USER_STORAGE_KEY); } function parseJwtPayload(token) { if (!token) { return null; } try { const [, payload] = token.split('.'); if (!payload) { return null; } const normalized = payload.replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized.padEnd(normalized.length + ((4 - normalized.length % 4) % 4), '='); return JSON.parse(window.atob(padded)); } catch { return null; } } function isLocalPreviewSession() { const isLocalHost = ['localhost', '127.0.0.1'].includes(window.location.hostname); if (!isLocalHost) { return false; } const payload = parseJwtPayload(localStorage.getItem('token')); return payload?.authSource === 'local-preview'; } function isPublicAuthPage() { return ['/login', '/callback', '/install'].includes(window.location.pathname); } function shouldBypass401Redirect(error) { const url = error?.config?.url || ''; return url.includes('/api/auth/verify') || isLocalPreviewSession(); } // Request interceptor request.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); // Response interceptor request.interceptors.response.use( (response) => { const res = response.data; // Compatible with backend interfaces that don't return code if (res && res.code && res.code !== 200) { const shouldBypassRedirect = res.code === 401 && shouldBypass401Redirect(response); if (!shouldBypassRedirect && !response.config?.silentError) { message.error(res.message || 'Error'); } if (res.code === 401 && !shouldBypassRedirect) { // Handle unauthorized - 但如果已在登录页则不重定向 clearAuthSession(); if (!isPublicAuthPage()) { if (!requestDesktopSessionRenewal('api-unauthorized')) { window.location.href = '/login'; } } } return Promise.reject(new Error(res.message || 'Error')); } clearReconnectNotice(); return res; }, async (error) => { const config = error?.config; // ---- 后端重启窗口内自动重试(只对幂等请求、只对网络错误/5xx)---- // 说明:这里用 `return request(config)`(不带 await)而不是 await, // 这样重试失败时由「内层」这一分支继续退避;外层调用栈不会重新进入本函数, // 因此最终失败提示天然只会出现一次,也不会把重试次数叠加错。 if (isRetryableRequest(config) && !config.__retryExhausted && isRetryableError(error)) { const attempt = Number(config.__retryAttempt || 0); if (attempt < RETRY_BACKOFF_MS.length) { config.__retryAttempt = attempt + 1; if (attempt === 0 && !config.silentError) { notifyReconnecting(); } await sleep(RETRY_BACKOFF_MS[attempt]); return request(config); } // 次数用尽:标记后不再重试,并把连接提示收掉 config.__retryExhausted = true; config.__retryAttempt = 0; clearReconnectNotice(); } const authSessionFailure = isAuthSessionFailure(error.response); if (!authSessionFailure && !error?.config?.silentError) { message.error(getRequestErrorMessage(error)); } if (authSessionFailure && !shouldBypass401Redirect(error)) { clearAuthSession(); // 如果已在登录页则不重定向,避免无限循环 if (!isPublicAuthPage()) { if (!requestDesktopSessionRenewal('api-unauthorized')) { window.location.href = '/login'; } } } return Promise.reject(error); } ); export default request;