4. BFF 错误对象标准化设计:统一错误响应结构、错误码枚举体系与国际化错误消息

在 BFF 架构中,错误处理的首要任务是建立一套标准化错误对象。没有标准化的错误响应,前端无法统一处理异常、监控系统无法聚合告警、排查人员无法快速定位根因。本章将深入讲解 BFF 错误对象的四要素结构、错误码枚举体系的设计原则以及国际化错误消息的实现方案。

核心原则:BFF 返回给客户端的每一个错误响应,都必须遵循相同的 JSON 结构,携带可枚举的错误码、人类可读的消息、可追踪的 traceId,以及可选的业务数据。

4.1 统一错误响应结构:四要素模型

我们定义 BFF 错误响应的标准 JSON 结构,包含四个核心字段:code、message、data、traceId。这个结构同时覆盖成功响应和错误响应,前端只需解析 code 字段即可判断请求状态。

字段 类型 必填 说明
code string 是 业务错误码,如 SUCCESS、PARAM_INVALID、USER_NOT_FOUND
message string 是 人类可读的错误描述,支持国际化
data object/null 是 成功时返回业务数据,失败时通常为 null 或包含额外错误详情
traceId string 是 全链路追踪 ID,用于关联日志和排查问题

以下是一个标准的错误响应示例:

{
  "code": "ORDER_PAY_TIMEOUT",
  "message": "订单支付超时,请重新提交",
  "data": {
    "orderId": "20250315001",
    "expiredAt": "2025-03-15T10:30:00Z"
  },
  "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
设计要点:即使请求成功,也返回 code: "SUCCESS" 和 traceId。这保证了前端拦截器可以统一处理所有响应,无需区分成功/失败分支。

4.2 错误码枚举体系:分层与分类

错误码是错误对象的灵魂。一个设计良好的错误码体系需要满足可枚举、可扩展、可归类三个特性。我们采用分层分类的编码方式,将错误码分为系统级、业务级和基础设施级三大类。

下图展示了 BFF 错误码的分层结构:

BFF 错误码分层体系 系统级错误 INTERNAL_ERROR SERVICE_UNAVAILABLE TIMEOUT RATE_LIMIT_EXCEEDED 业务级错误 PARAM_INVALID USER_NOT_FOUND ORDER_PAY_TIMEOUT INSUFFICIENT_BALANCE 基础设施错误 DB_CONNECTION_LOST CACHE_MISS MESSAGE_QUEUE_DOWN THIRD_PARTY_FAILURE 错误码格式:{分类前缀}_{具体错误名},全大写 + 下划线 示例:SYS_INTERNAL_ERROR、BIZ_PARAM_INVALID、INFRA_DB_CONNECTION_LOST

在实际项目中,我们建议将错误码定义为枚举常量,避免魔法字符串。以下是 Node.js 和 Go 两种语言的实现示例:

// Node.js / TypeScript 错误码枚举
export enum BffErrorCode {
  // 系统级
  INTERNAL_ERROR = 'SYS_INTERNAL_ERROR',
  SERVICE_UNAVAILABLE = 'SYS_SERVICE_UNAVAILABLE',
  TIMEOUT = 'SYS_TIMEOUT',
  RATE_LIMIT_EXCEEDED = 'SYS_RATE_LIMIT_EXCEEDED',

  // 业务级
  PARAM_INVALID = 'BIZ_PARAM_INVALID',
  USER_NOT_FOUND = 'BIZ_USER_NOT_FOUND',
  ORDER_PAY_TIMEOUT = 'BIZ_ORDER_PAY_TIMEOUT',
  INSUFFICIENT_BALANCE = 'BIZ_INSUFFICIENT_BALANCE',

  // 基础设施级
  DB_CONNECTION_LOST = 'INFRA_DB_CONNECTION_LOST',
  CACHE_MISS = 'INFRA_CACHE_MISS',
  MESSAGE_QUEUE_DOWN = 'INFRA_MESSAGE_QUEUE_DOWN',
  THIRD_PARTY_FAILURE = 'INFRA_THIRD_PARTY_FAILURE',
}
// Go 错误码常量
const (
    // 系统级
    ErrCodeInternalError      = "SYS_INTERNAL_ERROR"
    ErrCodeServiceUnavailable = "SYS_SERVICE_UNAVAILABLE"
    ErrCodeTimeout            = "SYS_TIMEOUT"
    ErrCodeRateLimitExceeded  = "SYS_RATE_LIMIT_EXCEEDED"

    // 业务级
    ErrCodeParamInvalid      = "BIZ_PARAM_INVALID"
    ErrCodeUserNotFound      = "BIZ_USER_NOT_FOUND"
    ErrCodeOrderPayTimeout   = "BIZ_ORDER_PAY_TIMEOUT"
    ErrCodeInsufficientBalance = "BIZ_INSUFFICIENT_BALANCE"

    // 基础设施级
    ErrCodeDbConnectionLost    = "INFRA_DB_CONNECTION_LOST"
    ErrCodeCacheMiss           = "INFRA_CACHE_MISS"
    ErrCodeMessageQueueDown    = "INFRA_MESSAGE_QUEUE_DOWN"
    ErrCodeThirdPartyFailure   = "INFRA_THIRD_PARTY_FAILURE"
)

4.3 国际化错误消息:多语言支持

BFF 服务可能面向全球用户,错误消息必须支持国际化。我们采用键值对 + 语言环境的方案,将错误码映射到不同语言的消息模板。

国际化消息的设计要点:

  • 消息模板化:消息中可包含占位符,如 {paramName}、{userId},运行时动态替换。
  • 语言回退:当请求的语言不存在时,默认回退到英文或中文。
  • 前端传递语言偏好:通过 HTTP 请求头 Accept-Language 或自定义头 X-Locale 传递。

以下是一个国际化消息配置文件的示例(JSON 格式):

{
  "en": {
    "BIZ_PARAM_INVALID": "Invalid parameter: {paramName}",
    "BIZ_USER_NOT_FOUND": "User not found with id: {userId}",
    "BIZ_ORDER_PAY_TIMEOUT": "Order payment timeout, please resubmit",
    "SYS_INTERNAL_ERROR": "Internal server error, please try again later"
  },
  "zh-CN": {
    "BIZ_PARAM_INVALID": "参数无效:{paramName}",
    "BIZ_USER_NOT_FOUND": "未找到用户,ID:{userId}",
    "BIZ_ORDER_PAY_TIMEOUT": "订单支付超时,请重新提交",
    "SYS_INTERNAL_ERROR": "服务器内部错误,请稍后重试"
  },
  "ja": {
    "BIZ_PARAM_INVALID": "パラメータが無効です:{paramName}",
    "BIZ_USER_NOT_FOUND": "ユーザーが見つかりません:{userId}",
    "BIZ_ORDER_PAY_TIMEOUT": "注文の支払いがタイムアウトしました。再送信してください",
    "SYS_INTERNAL_ERROR": "サーバー内部エラーが発生しました。後でもう一度お試しください"
  }
}

在 BFF 层,我们通过一个 I18nService 来解析语言并填充消息:

// Node.js 国际化消息服务示例
class I18nService {
  private messages: Record<string, Record<string, string>>;

  constructor(messages: Record<string, Record<string, string>>) {
    this.messages = messages;
  }

  getMessage(code: string, locale: string, params?: Record<string, string>): string {
    const langMessages = this.messages[locale] || this.messages['en'];
    let template = langMessages[code] || code;
    if (params) {
      for (const [key, value] of Object.entries(params)) {
        template = template.replace(`{${key}}`, value);
      }
    }
    return template;
  }
}
避坑指南:不要在 BFF 层做完整的翻译引擎,那会引入不必要的复杂度。BFF 只负责根据语言偏好选择预定义的消息模板并填充参数。完整的翻译管理应交给专门的国际化平台或配置文件。

4.4 错误对象生成流程:从异常到标准化响应

下图展示了 BFF 中一个请求从抛出异常到返回标准化错误对象的完整流程:

BFF 错误对象生成流程 客户端请求 BFF 中间件 业务逻辑执行 构建成功响应 捕获异常 标准化错误对象

流程中的关键步骤:

  1. 请求进入 BFF 中间件:中间件解析 Accept-Language 头,生成 traceId,并注入请求上下文。
  2. 业务逻辑执行:调用下游服务或本地计算,可能抛出业务异常或系统异常。
  3. 异常捕获与转换:全局错误处理中间件捕获所有异常,根据异常类型映射到对应的错误码。
  4. 构建标准化响应:调用 I18nService 获取国际化消息,组装 { code, message, data, traceId } 对象返回。

4.5 最佳实践与常见陷阱

实践 说明
错误码必须唯一且稳定 一旦发布,错误码不应被删除或修改含义,只能废弃(deprecated)。前端和监控系统可能硬编码了错误码。
区分客户端错误和服务端错误 4xx 错误(如参数校验失败)不应记录为系统告警;5xx 错误(如数据库连接丢失)必须触发告警。
不要在 message 中暴露敏感信息 如数据库表名、堆栈信息、内部 IP。敏感信息应记录在日志中,而非返回给客户端。
traceId 必须贯穿全链路 从 BFF 入口生成,传递给下游服务,并在日志中输出。确保错误排查时可以串联所有调用链。
国际化消息不要硬编码在代码中 使用外部配置文件或配置中心管理,支持热更新,避免因修改消息而重新部署服务。
推荐做法:在 BFF 的全局错误处理中间件中,统一捕获 BffError 类型(自定义错误类),其他未知异常统一映射为 SYS_INTERNAL_ERROR。这样可以保证所有错误响应都经过标准化流程,不会遗漏。

通过本章的设计,BFF 服务将具备统一的错误响应结构、清晰的错误码枚举体系以及灵活的国际化消息能力。这为后续的故障排查、监控告警和前端统一处理奠定了坚实的基础。