2
0

error-handling.md 12 KB

D8D Starter 错误处理规范

版本信息

版本 日期 描述 作者
1.0 2025-09-15 初始错误处理规范 Sarah (PO)

1. 错误处理原则

1.1 核心原则

  • 一致性: 所有错误响应格式统一
  • 安全性: 不泄露敏感信息到客户端
  • 可操作性: 错误信息应指导用户或开发者
  • 可追溯性: 错误应包含足够上下文用于调试

1.2 错误分类

按严重程度

  • 🔴 致命错误: 系统无法继续运行
  • 🟡 业务错误: 用户操作失败,可恢复
  • 🔵 客户端错误: 用户输入或配置问题
  • 信息性错误: 警告或提示信息

按来源

  • 验证错误: 输入数据验证失败
  • 业务逻辑错误: 业务规则违反
  • 系统错误: 基础设施或第三方服务故障
  • 网络错误: 连接超时或中断

2. 错误响应格式

2.1 标准错误响应

// 成功响应
{
  "success": true,
  "data": { /* 业务数据 */ },
  "message": "操作成功"
}

// 错误响应
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "输入数据验证失败",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确"
      }
    ],
    "timestamp": "2025-09-15T10:30:00Z",
    "requestId": "req_1234567890"
  }
}

2.2 HTTP状态码映射

错误类型 HTTP状态码 错误代码 描述
验证错误 400 VALIDATION_ERROR 输入数据验证失败
未授权 401 UNAUTHORIZED 需要认证
权限不足 403 FORBIDDEN 权限不足
资源不存在 404 NOT_FOUND 资源未找到
业务错误 409 BUSINESS_ERROR 业务规则冲突
系统错误 500 INTERNAL_ERROR 服务器内部错误
服务不可用 503 SERVICE_UNAVAILABLE 服务暂时不可用

3. 错误代码规范

3.1 错误代码命名约定

  • 使用 SCREAMING_SNAKE_CASE
  • 前缀表示错误类别
  • 后缀表示具体错误

3.2 标准错误代码

认证错误 (AUTH_*)

export const AUTH_ERRORS = {
  UNAUTHORIZED: 'AUTH_UNAUTHORIZED',
  INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
  TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
  TOKEN_INVALID: 'AUTH_TOKEN_INVALID',
  PERMISSION_DENIED: 'AUTH_PERMISSION_DENIED'
} as const;

验证错误 (VALIDATION_*)

export const VALIDATION_ERRORS = {
  REQUIRED_FIELD: 'VALIDATION_REQUIRED_FIELD',
  INVALID_EMAIL: 'VALIDATION_INVALID_EMAIL',
  INVALID_PASSWORD: 'VALIDATION_INVALID_PASSWORD',
  UNIQUE_CONSTRAINT: 'VALIDATION_UNIQUE_CONSTRAINT',
  OUT_OF_RANGE: 'VALIDATION_OUT_OF_RANGE'
} as const;

业务错误 (BUSINESS_*)

export const BUSINESS_ERRORS = {
  USER_NOT_FOUND: 'BUSINESS_USER_NOT_FOUND',
  INSUFFICIENT_BALANCE: 'BUSINESS_INSUFFICIENT_BALANCE',
  OPERATION_NOT_ALLOWED: 'BUSINESS_OPERATION_NOT_ALLOWED',
  RESOURCE_CONFLICT: 'BUSINESS_RESOURCE_CONFLICT'
} as const;

系统错误 (SYSTEM_*)

export const SYSTEM_ERRORS = {
  DATABASE_ERROR: 'SYSTEM_DATABASE_ERROR',
  NETWORK_ERROR: 'SYSTEM_NETWORK_ERROR',
  EXTERNAL_SERVICE_ERROR: 'SYSTEM_EXTERNAL_SERVICE_ERROR',
  CONFIGURATION_ERROR: 'SYSTEM_CONFIGURATION_ERROR'
} as const;

4. 实现指南

4.1 后端错误处理

错误类定义

// src/shared/errors/AppError.ts
export class AppError extends Error {
  public readonly code: string;
  public readonly statusCode: number;
  public readonly details?: any;
  public readonly timestamp: Date;

  constructor(
    code: string,
    message: string,
    statusCode: number = 500,
    details?: any
  ) {
    super(message);
    this.code = code;
    this.statusCode = statusCode;
    this.details = details;
    this.timestamp = new Date();

    // 保持正确的堆栈跟踪
    Error.captureStackTrace(this, this.constructor);
  }

  toJSON() {
    return {
      code: this.code,
      message: this.message,
      details: this.details,
      timestamp: this.timestamp.toISOString()
    };
  }
}

// 特定错误类
export class ValidationError extends AppError {
  constructor(message: string, details?: any) {
    super('VALIDATION_ERROR', message, 400, details);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string, id?: string | number) {
    const message = id
      ? `${resource} with ID ${id} not found`
      : `${resource} not found`;
    super('NOT_FOUND', message, 404);
  }
}

错误处理中间件

// src/server/middleware/errorHandler.ts
import { Context } from 'hono';
import { AppError } from '../../shared/errors/AppError';

export async function errorHandler(err: Error, c: Context) {
  // 记录错误
  console.error('Error:', {
    message: err.message,
    stack: err.stack,
    url: c.req.url,
    method: c.req.method,
    timestamp: new Date().toISOString()
  });

  // AppError 实例
  if (err instanceof AppError) {
    return c.json(
      {
        success: false,
        error: {
          code: err.code,
          message: err.message,
          details: err.details,
          timestamp: err.timestamp.toISOString(),
          requestId: c.get('requestId')
        }
      },
      err.statusCode
    );
  }

  // 未知错误(生产环境隐藏细节)
  const isProduction = process.env.NODE_ENV === 'production';

  return c.json(
    {
      success: false,
      error: {
        code: 'INTERNAL_ERROR',
        message: isProduction ? 'Internal server error' : err.message,
        timestamp: new Date().toISOString(),
        requestId: c.get('requestId')
      }
    },
    500
  );
}

使用示例

// 在服务中使用
export class UserService {
  async getUserById(id: number) {
    const user = await User.findOne({ where: { id } });

    if (!user) {
      throw new NotFoundError('User', id);
    }

    return user;
  }

  async createUser(data: CreateUserDto) {
    // 验证输入
    const errors = validateUserData(data);
    if (errors.length > 0) {
      throw new ValidationError('Invalid user data', errors);
    }

    // 检查唯一性
    const existingUser = await User.findOne({
      where: [{ email: data.email }, { username: data.username }]
    });

    if (existingUser) {
      throw new BusinessError('USER_ALREADY_EXISTS', 'User with this email or username already exists', 409);
    }

    // 创建用户
    try {
      return await User.create(data).save();
    } catch (error) {
      throw new DatabaseError('Failed to create user', error);
    }
  }
}

4.2 前端错误处理

错误处理钩子

// src/client/hooks/useErrorHandler.ts
import { toast } from 'sonner';

export function useErrorHandler() {
  const handleError = (error: unknown) => {
    console.error('Application error:', error);

    if (error instanceof Error) {
      // 显示用户友好的错误消息
      toast.error(getUserFriendlyMessage(error));
    } else {
      toast.error('发生未知错误,请重试');
    }
  };

  const getUserFriendlyMessage = (error: Error): string => {
    const message = error.message.toLowerCase();

    if (message.includes('network')) {
      return '网络连接失败,请检查网络设置';
    }

    if (message.includes('validation')) {
      return '输入数据验证失败,请检查表单';
    }

    if (message.includes('permission') || message.includes('auth')) {
      return '权限不足,请重新登录';
    }

    return error.message || '操作失败,请重试';
  };

  return { handleError, getUserFriendlyMessage };
}

API客户端错误处理

// src/client/api.ts
import { toast } from 'sonner';

const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
  try {
    const response = await axios.request({
      url: url.toString(),
      method: init?.method || 'GET',
      headers: init?.headers,
      data: init?.body,
    });

    // 处理错误响应
    if (response.status >= 400) {
      const errorData = response.data?.error || {};
      throw new ApiError(
        errorData.message || '请求失败',
        response.status,
        errorData.code,
        errorData.details
      );
    }

    return createResponse(response);
  } catch (error) {
    if (axios.isAxiosError(error)) {
      const errorData = error.response?.data?.error || {};
      throw new ApiError(
        errorData.message || error.message,
        error.response?.status || 500,
        errorData.code,
        errorData.details
      );
    }
    throw error;
  }
};

class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public code?: string,
    public details?: any
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

5. 日志和监控

5.1 错误日志格式

// 错误日志示例
{
  "level": "error",
  "timestamp": "2025-09-15T10:30:00.000Z",
  "message": "User creation failed",
  "code": "VALIDATION_ERROR",
  "stack": "Error: Invalid email format...",
  "context": {
    "userId": 123,
    "email": "invalid-email",
    "requestId": "req_1234567890",
    "url": "/api/v1/users",
    "method": "POST"
  }
}

5.2 监控配置

// 错误监控集成
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
});

// 在错误处理中间件中
if (process.env.NODE_ENV === 'production') {
  Sentry.captureException(err, {
    extra: {
      url: c.req.url,
      method: c.req.method,
      requestId: c.get('requestId')
    }
  });
}

6. 测试错误场景

6.1 错误测试示例

// 测试验证错误
describe('UserService - Error Handling', () => {
  it('should throw ValidationError for invalid email', async () => {
    const invalidData = { email: 'invalid', password: '123' };

    await expect(userService.createUser(invalidData))
      .rejects
      .toThrow(ValidationError);

    await expect(userService.createUser(invalidData))
      .rejects
      .toMatchObject({
        code: 'VALIDATION_ERROR',
        statusCode: 400
      });
  });

  it('should throw NotFoundError for non-existent user', async () => {
    await expect(userService.getUserById(999))
      .rejects
      .toThrow(NotFoundError);
  });
});

// 测试错误响应格式
describe('Error Handler Middleware', () => {
  it('should return formatted error response', async () => {
    const server = createTestServer();

    const response = await server.inject({
      method: 'GET',
      url: '/api/v1/users/999'
    });

    expect(response.statusCode).toBe(404);
    expect(response.json()).toEqual({
      success: false,
      error: {
        code: 'NOT_FOUND',
        message: 'User with ID 999 not found',
        timestamp: expect.any(String),
        requestId: expect.any(String)
      }
    });
  });
});

7. 最佳实践清单

7.1 错误处理检查清单

  • 所有错误都有明确的错误代码
  • 错误消息对用户友好且可操作
  • 生产环境不泄露敏感信息
  • 错误包含足够的调试上下文
  • 错误响应格式统一
  • 适当的HTTP状态码
  • 错误日志记录完整
  • 前端错误处理用户友好
  • 测试覆盖错误场景

7.2 安全注意事项

  • ❌ 不要返回堆栈跟踪到客户端
  • ❌ 不要泄露数据库错误细节
  • ❌ 不要暴露敏感配置信息
  • ✅ 使用通用的错误消息
  • ✅ 记录详细错误到服务器日志
  • ✅ 使用请求ID关联错误

最后更新: 2025-09-15 维护者: 开发团队 文档状态: 正式版