This commit is contained in:
2026-08-06 18:02:02 +08:00
parent 9aa79bef6d
commit 20f8a436e0
6 changed files with 69 additions and 31 deletions
+16
View File
@@ -0,0 +1,16 @@
export type ApiFieldErrors = Record<string, string>
export function asFieldErrors(value: unknown): ApiFieldErrors | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
const entries = Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
return entries.length > 0 ? Object.fromEntries(entries) : undefined
}
export function apiErrorMessage(message: string, fieldErrors?: ApiFieldErrors) {
const baseMessage = message.trim() || '请求失败'
const entries = Object.entries(fieldErrors ?? {})
if (entries.length === 0) return baseMessage
if (entries.every(([field, detail]) => baseMessage.includes(field) && baseMessage.includes(detail))) return baseMessage
const details = entries.map(([field, detail]) => `${field}: ${detail}`).join(';')
return `${baseMessage}:${details}`
}
+7 -4
View File
@@ -1,7 +1,8 @@
import axios, { AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
import { parseApiJsonResponse } from '@chongchong/shared'
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTokenPair, type TokenPair } from '@/api/session'
import { ApiError, asFieldErrors, isApiEnvelope, type ApiEnvelope } from '@/api/types'
import { apiErrorMessage, asFieldErrors } from '@/api/errorDetails'
import { ApiError, isApiEnvelope, type ApiEnvelope } from '@/api/types'
const transport = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
@@ -15,7 +16,8 @@ interface RetryableRequest extends InternalAxiosRequestConfig {
function unwrap<T>(envelope: ApiEnvelope<T>) {
if (envelope.code !== '200') {
throw new ApiError(envelope.message || '请求失败', envelope.code, undefined, asFieldErrors(envelope.data), envelope.traceId)
const fieldErrors = asFieldErrors(envelope.data)
throw new ApiError(apiErrorMessage(envelope.message, fieldErrors), envelope.code, undefined, fieldErrors, envelope.traceId)
}
return envelope.data
}
@@ -26,11 +28,12 @@ function mapError(error: unknown) {
const body = error.response?.data
if (isApiEnvelope(body)) {
const fieldErrors = asFieldErrors(body.data)
return new ApiError(
body.message || '请求失败',
apiErrorMessage(body.message, fieldErrors),
body.code,
error.response?.status,
asFieldErrors(body.data),
fieldErrors,
typeof body.traceId === 'string' ? body.traceId : null,
)
}
+19
View File
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { apiErrorMessage, asFieldErrors } from './errorDetails.ts'
test('adds validation field details to a generic API message', () => {
const errors = asFieldErrors({ carrierName: '不能为空', carrierId: '不能为null' })
assert.equal(
apiErrorMessage('Validation failed', errors),
'Validation failed:carrierName: 不能为空;carrierId: 不能为null',
)
})
test('does not duplicate validation details already included by the server', () => {
const errors = { carrierName: '不能为空', carrierId: '不能为null' }
const message = 'carrierName: 不能为空;carrierId: 不能为null'
assert.equal(apiErrorMessage(message, errors), message)
})
+3 -7
View File
@@ -1,3 +1,5 @@
import type { ApiFieldErrors } from '@/api/errorDetails'
export interface ApiEnvelope<T> {
code: string
message: string
@@ -7,7 +9,7 @@ export interface ApiEnvelope<T> {
timestamp: string
}
export type ApiFieldErrors = Record<string, string>
export type { ApiFieldErrors } from '@/api/errorDetails'
export class ApiError extends Error {
constructor(
@@ -27,9 +29,3 @@ export function isApiEnvelope(value: unknown): value is ApiEnvelope<unknown> {
const record = value as Record<string, unknown>
return typeof record.code === 'string' && typeof record.message === 'string' && 'data' in record
}
export function asFieldErrors(value: unknown): ApiFieldErrors | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
const entries = Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
return entries.length > 0 ? Object.fromEntries(entries) : undefined
}