⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

7.2.4 错误响应格式

一句话破题

好的错误响应要回答三个问题:错了什么(code)、为什么错(message)、怎么修复(details)。

统一错误格式

typescript
interface ErrorResponse {
  error: {
    code: string           // 机器可读的错误码
    message: string        // 用户可读的错误信息
    details?: ErrorDetail[] // 详细的字段级错误
    traceId?: string       // 请求追踪 ID
  }
}

interface ErrorDetail {
  field: string            // 出错的字段
  message: string          // 字段错误信息
  code?: string            // 字段错误码
}

示例

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确", "code": "INVALID_FORMAT" },
      { "field": "password", "message": "密码至少 8 个字符", "code": "TOO_SHORT" }
    ],
    "traceId": "abc-123-xyz"
  }
}

错误码设计

命名规范

typescript
// 使用大写下划线分隔
const ErrorCodes = {
  // 认证相关
  UNAUTHORIZED: 'UNAUTHORIZED',
  TOKEN_EXPIRED: 'TOKEN_EXPIRED',
  INVALID_TOKEN: 'INVALID_TOKEN',
  
  // 权限相关
  FORBIDDEN: 'FORBIDDEN',
  INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
  
  // 资源相关
  NOT_FOUND: 'NOT_FOUND',
  ALREADY_EXISTS: 'ALREADY_EXISTS',
  
  // 验证相关
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  INVALID_FORMAT: 'INVALID_FORMAT',
  REQUIRED_FIELD: 'REQUIRED_FIELD',
  
  // 业务相关
  INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
  ORDER_CANCELLED: 'ORDER_CANCELLED',
  
  // 系统相关
  INTERNAL_ERROR: 'INTERNAL_ERROR',
  SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
  RATE_LIMITED: 'RATE_LIMITED',
} as const

错误码与状态码对应

错误码HTTP 状态码说明
UNAUTHORIZED401未认证
TOKEN_EXPIRED401Token 过期
FORBIDDEN403无权限
NOT_FOUND404资源不存在
ALREADY_EXISTS409资源已存在
VALIDATION_ERROR400/422验证失败
RATE_LIMITED429限流
INTERNAL_ERROR500服务器错误

实现统一错误处理

错误类定义

typescript
// lib/errors.ts
export class AppError extends Error {
  constructor(
    public code: string,
    message: string,
    public statusCode: number = 400,
    public details?: ErrorDetail[]
  ) {
    super(message)
    this.name = 'AppError'
  }
}

export class ValidationError extends AppError {
  constructor(details: ErrorDetail[]) {
    super('VALIDATION_ERROR', '请求参数验证失败', 400, details)
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super('NOT_FOUND', `${resource}不存在`, 404)
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = '请先登录') {
    super('UNAUTHORIZED', message, 401)
  }
}

export class ForbiddenError extends AppError {
  constructor(message = '无权访问此资源') {
    super('FORBIDDEN', message, 403)
  }
}

错误响应工具

typescript
// lib/api-response.ts
export function errorResponse(error: AppError, traceId?: string) {
  return NextResponse.json(
    {
      error: {
        code: error.code,
        message: error.message,
        details: error.details,
        traceId,
      },
    },
    { status: error.statusCode }
  )
}

export function successResponse<T>(data: T, status = 200) {
  return NextResponse.json({ data }, { status })
}

API 路由使用

typescript
// app/api/users/route.ts
import { AppError, ValidationError, NotFoundError } from '@/lib/errors'
import { errorResponse, successResponse } from '@/lib/api-response'

export async function POST(request: NextRequest) {
  const traceId = crypto.randomUUID()
  
  try {
    const body = await request.json()
    
    // 验证
    const errors: ErrorDetail[] = []
    if (!body.email) {
      errors.push({ field: 'email', message: '邮箱不能为空', code: 'REQUIRED' })
    }
    if (!body.password || body.password.length < 8) {
      errors.push({ field: 'password', message: '密码至少 8 个字符', code: 'TOO_SHORT' })
    }
    
    if (errors.length > 0) {
      throw new ValidationError(errors)
    }
    
    // 检查邮箱是否已存在
    const existing = await prisma.user.findUnique({
      where: { email: body.email },
    })
    if (existing) {
      throw new AppError('EMAIL_EXISTS', '邮箱已被注册', 409)
    }
    
    // 创建用户
    const user = await prisma.user.create({ data: body })
    return successResponse(user, 201)
    
  } catch (error) {
    if (error instanceof AppError) {
      return errorResponse(error, traceId)
    }
    
    console.error(`[${traceId}] Unexpected error:`, error)
    return errorResponse(
      new AppError('INTERNAL_ERROR', '服务器内部错误', 500),
      traceId
    )
  }
}

前端错误处理

typescript
// lib/api-client.ts
interface ApiError {
  code: string
  message: string
  details?: { field: string; message: string }[]
  traceId?: string
}

async function fetchApi<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, options)
  const data = await response.json()
  
  if (!response.ok) {
    const error = data.error as ApiError
    throw new ApiClientError(error)
  }
  
  return data.data
}

class ApiClientError extends Error {
  constructor(public error: ApiError) {
    super(error.message)
  }
  
  // 获取字段错误
  getFieldError(field: string): string | undefined {
    return this.error.details?.find(d => d.field === field)?.message
  }
  
  // 判断错误类型
  isUnauthorized() {
    return this.error.code === 'UNAUTHORIZED'
  }
  
  isValidationError() {
    return this.error.code === 'VALIDATION_ERROR'
  }
}

表单中使用

typescript
// 表单提交
async function handleSubmit(data: FormData) {
  try {
    await fetchApi('/api/users', {
      method: 'POST',
      body: JSON.stringify(data),
    })
    toast.success('注册成功')
  } catch (error) {
    if (error instanceof ApiClientError) {
      if (error.isValidationError()) {
        // 显示字段错误
        setErrors({
          email: error.getFieldError('email'),
          password: error.getFieldError('password'),
        })
      } else {
        toast.error(error.message)
      }
    }
  }
}

国际化支持

typescript
// lib/error-messages.ts
const errorMessages: Record<string, Record<string, string>> = {
  zh: {
    UNAUTHORIZED: '请先登录',
    TOKEN_EXPIRED: '登录已过期,请重新登录',
    FORBIDDEN: '无权访问此资源',
    NOT_FOUND: '资源不存在',
    VALIDATION_ERROR: '请求参数验证失败',
    INTERNAL_ERROR: '服务器内部错误',
  },
  en: {
    UNAUTHORIZED: 'Please login first',
    TOKEN_EXPIRED: 'Login expired, please login again',
    FORBIDDEN: 'Access denied',
    NOT_FOUND: 'Resource not found',
    VALIDATION_ERROR: 'Validation failed',
    INTERNAL_ERROR: 'Internal server error',
  },
}

export function getErrorMessage(code: string, locale = 'zh'): string {
  return errorMessages[locale]?.[code] || errorMessages.zh[code] || code
}

觉知:常见问题

1. 错误信息不一致

typescript
// ❌ 每个接口返回格式不同
{ "error": "用户不存在" }
{ "message": "参数错误" }
{ "err": { "msg": "失败" } }

// ✅ 统一格式
{
  "error": {
    "code": "NOT_FOUND",
    "message": "用户不存在"
  }
}

2. 暴露敏感信息

typescript
// ❌ 暴露内部实现细节
{
  "error": {
    "message": "查询失败:SELECT * FROM users WHERE id = '1' OR '1'='1'"
  }
}

// ✅ 返回通用信息
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "服务器内部错误",
    "traceId": "abc-123"  // 用于日志追踪
  }
}

3. 验证错误不够具体

typescript
// ❌ 只返回"验证失败"
{ "error": { "message": "验证失败" } }

// ✅ 返回详细的字段错误
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      { "field": "email", "message": "邮箱格式不正确" },
      { "field": "age", "message": "年龄必须大于 0" }
    ]
  }
}

本节小结

要点说明
统一格式code + message + details
错误码大写下划线,机器可读
错误类封装常见错误类型
前端处理根据 code 做不同处理