mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-31 21:21:50 +08:00
fix(api): 重启期间抑制离线提示并收敛连接类错误反馈
- 网关错误(502/503/504)归为连接失败上报离线探测,不再逐请求弹Toast - 仅成功响应标记服务在线,避免重启期间干扰离线阈值累计 - 新增全局重启状态,重启期间不累计离线阈值且不弹统一离线提示
This commit is contained in:
@@ -195,6 +195,49 @@ describe('MoviePilot API client', () => {
|
||||
expect(reportConnectionFailure).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([502, 503, 504])('网关错误 %d 上报连接失败且不弹请求层 Toast', async status => {
|
||||
const reportConnectionFailure = vi.fn()
|
||||
const { api } = createApiClients({
|
||||
adapter: rejectWith({ message: 'Gateway unavailable' }, status),
|
||||
hooks: { reportConnectionFailure },
|
||||
notifier,
|
||||
})
|
||||
|
||||
const error = requireApiRequestError(await api.get('/resource').catch(reason => reason))
|
||||
|
||||
expect(error.status).toBe(status)
|
||||
expect(reportConnectionFailure).toHaveBeenCalledWith('server-unreachable')
|
||||
expect(notifier.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('网络错误仅上报离线状态,不弹请求层 Toast', async () => {
|
||||
const reportConnectionFailure = vi.fn()
|
||||
const adapter: AxiosAdapter = async () => {
|
||||
throw new AxiosError('Network Error', AxiosError.ERR_NETWORK)
|
||||
}
|
||||
const { api } = createApiClients({ adapter, hooks: { reportConnectionFailure }, notifier })
|
||||
|
||||
await api.get('/resource').catch(reason => reason)
|
||||
|
||||
expect(reportConnectionFailure).toHaveBeenCalledWith('network-error')
|
||||
expect(notifier.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('网关错误不算服务在线证据,仅成功响应恢复在线状态', async () => {
|
||||
const markServerOnline = vi.fn()
|
||||
const reportConnectionFailure = vi.fn()
|
||||
const { api } = createApiClients({
|
||||
adapter: rejectWith({ message: 'Bad Gateway' }, 502),
|
||||
hooks: { markServerOnline, reportConnectionFailure },
|
||||
notifier,
|
||||
})
|
||||
|
||||
await api.get('/resource').catch(reason => reason)
|
||||
|
||||
expect(markServerOnline).not.toHaveBeenCalled()
|
||||
expect(reportConnectionFailure).toHaveBeenCalledWith('server-unreachable')
|
||||
})
|
||||
|
||||
it('拒绝缺少标准字段的普通 JSON 响应', async () => {
|
||||
const { api } = createApiClients({ adapter: resolveWith({ value: 1 }), notifier })
|
||||
|
||||
|
||||
+18
-6
@@ -78,7 +78,7 @@ export interface ApiFeedbackNotifier {
|
||||
export interface ApiClientHooks {
|
||||
markServerOnline?(): void
|
||||
onForbidden?(error: ApiRequestError): void
|
||||
reportConnectionFailure?(reason: 'network-error' | 'timeout'): void
|
||||
reportConnectionFailure?(reason: 'network-error' | 'timeout' | 'server-unreachable'): void
|
||||
}
|
||||
|
||||
/** 创建内部数据客户端与插件原始协议客户端时所需的配置。 */
|
||||
@@ -158,12 +158,21 @@ export function isApiResponse<T = unknown>(value: unknown): value is ApiResponse
|
||||
return typeof record.success === 'boolean' && typeof record.message === 'string' && 'data' in record
|
||||
}
|
||||
|
||||
/** 将 Axios 连接错误归类为全局服务探测可识别的原因。 */
|
||||
export function resolveConnectionFailureReason(error: AxiosError): 'network-error' | 'timeout' | null {
|
||||
/**
|
||||
* 将 Axios 连接错误归类为全局服务探测可识别的原因。
|
||||
*
|
||||
* 网关不可用状态码(502/503/504)同样视为服务不可达:后端重启或崩溃时网关
|
||||
* 会返回这类响应,若只按“无响应”判断会漏掉重启场景的离线检测。
|
||||
*/
|
||||
export function resolveConnectionFailureReason(
|
||||
error: AxiosError,
|
||||
): 'network-error' | 'timeout' | 'server-unreachable' | null {
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') return 'timeout'
|
||||
if (error.code === 'NETWORK_ERROR' || error.code === 'ERR_NETWORK' || error.name === 'NetworkError') {
|
||||
return 'network-error'
|
||||
}
|
||||
const status = error.response?.status
|
||||
if (status === 502 || status === 503 || status === 504) return 'server-unreachable'
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -241,7 +250,8 @@ function installResponseInterceptors(
|
||||
|
||||
const original = reason instanceof AxiosError ? reason : undefined
|
||||
const response = original?.response ? await normalizeErrorResponse(original.response) : undefined
|
||||
if (response) hooks?.markServerOnline?.()
|
||||
// 只有成功响应才算服务在线证据;网关错误响应说明后端当前不可达,不能恢复在线状态。
|
||||
if (response && response.status >= 200 && response.status < 300) hooks?.markServerOnline?.()
|
||||
|
||||
const payload = response?.data
|
||||
const error = new ApiRequestError(resolveErrorMessage(payload, original, resolveFallbackMessage), {
|
||||
@@ -255,12 +265,14 @@ function installResponseInterceptors(
|
||||
|
||||
const requestConfig = original?.config
|
||||
const failureReason = original ? resolveConnectionFailureReason(original) : null
|
||||
if (!response && !requestConfig?.skipConnectionTracking && failureReason) {
|
||||
if (!requestConfig?.skipConnectionTracking && failureReason) {
|
||||
hooks?.reportConnectionFailure?.(failureReason)
|
||||
}
|
||||
if (response?.status === 403) hooks?.onForbidden?.(error)
|
||||
|
||||
notifyFailure(requestConfig?.feedback, notifier, error.message)
|
||||
// 连接类失败(无响应、超时、网关不可用)统一交给离线状态系统按阈值提示,
|
||||
// 不在请求层逐个弹出,避免后端重启时刷屏。
|
||||
if (!failureReason) notifyFailure(requestConfig?.feedback, notifier, error.message)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user