Files
codex-proxy-rs/frontend/src/api/error.ts
T
ZyyandCodex 322b602154 fix(frontend): centralize admin API error notifications
Show server envelope messages from the response interceptor, with shared transport fallbacks, cancellation handling, and explicit silent requests. Remove duplicate page notifications while preserving contextual batch and irreversible-operation feedback.

Propagate request options through API modules and document notification ownership. Verify the notification-only tree with ESLint, vue-tsc, and a Vite build.

Co-authored-by: Codex <noreply@openai.com>
2026-09-12 23:09:04 +08:00

109 lines
3.4 KiB
TypeScript
Raw 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 type { AxiosError, AxiosResponse, AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios'
export type ApiErrorKind = 'api' | 'timeout' | 'network' | 'http' | 'cancelled'
interface AdminErrorEnvelope {
code: number
message: string
}
export class ApiError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly code?: number,
public readonly requestId?: string,
public readonly kind: ApiErrorKind = 'api',
) {
super(message)
this.name = 'ApiError'
}
}
export function normalizeApiError(error: AxiosError<unknown>) {
const status = error.response?.status ?? 0
const requestId = responseHeader(error.response?.headers, 'x-request-id')
if (error.code === 'ERR_CANCELED') {
return new ApiError('请求已取消', status, undefined, requestId, 'cancelled')
}
const apiError = error.response && normalizeApiResponseError(error.response)
if (apiError)
return apiError
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || error.code === 'ERR_TIMEOUT') {
return new ApiError('请求超时,请稍后重试', status, undefined, requestId, 'timeout')
}
if (!error.response) {
return new ApiError('网络连接失败,请检查网络后重试', 0, undefined, requestId, 'network')
}
return new ApiError(httpFallback(status), status, undefined, requestId, 'http')
}
export function normalizeApiResponseError(response: AxiosResponse<unknown>) {
const envelope = adminErrorEnvelope(response.data)
if (!envelope || envelope.code === 200)
return undefined
return new ApiError(
envelope.message || httpFallback(response.status >= 400 ? response.status : 0),
response.status,
envelope.code,
responseHeader(response.headers, 'x-request-id'),
'api',
)
}
function adminErrorEnvelope(value: unknown): AdminErrorEnvelope | null {
if (!value || typeof value !== 'object')
return null
const record = value as Record<string, unknown>
if (typeof record.code !== 'number' || !Number.isSafeInteger(record.code))
return null
const message = typeof record.message === 'string' ? record.message.trim() : ''
return { code: record.code, message }
}
function responseHeader(
headers: RawAxiosResponseHeaders | AxiosResponseHeaders | undefined,
name: string,
) {
if (!headers)
return undefined
const value = typeof headers.get === 'function' ? headers.get(name) : headers[name]
return typeof value === 'string' && value ? value : undefined
}
function httpFallback(status: number) {
switch (status) {
case 400:
return '请求参数不合法'
case 401:
return '需要管理员登录'
case 403:
return '没有权限执行此操作'
case 404:
return '请求的接口不存在'
case 408:
return '请求超时,请稍后重试'
case 409:
return '资源状态冲突,请刷新后重试'
case 413:
return '请求内容过大'
case 415:
return '请求格式不受支持'
case 422:
return '请求内容不合法'
case 429:
return '请求过于频繁,请稍后重试'
case 500:
return '服务内部错误'
case 502:
return '上游服务请求失败'
case 503:
return '服务暂不可用,请稍后重试'
case 504:
return '上游服务响应超时'
default:
return status > 0 ? `请求失败(HTTP ${status})` : '请求失败'
}
}