百积木文档
开发集成

CModel 错误模型

CModel Error Contract 1.0.0 的三字段响应结构、HTTP 语义和调用规则。

CModel Error Contract 1.0.0 用于百积木拥有的非流式 HTTP 和 Module 调用边界。公共响应只包含 contractVersion、errorCode 和 data 三个字段;服务不能再输出 value、systemCurrentTime、error 或 { success, message }、{ code, message } 等私有响应。

公共机器可读源事实与 SDK:

每个公开 API 必须在自身文档中登记调用方能够观察和处理的稳定 errorCode。平台内部可以保留错误目录、分类、诊断信息和类型,但这些内部字段不能序列化进公共响应,也不能改变 CModel 固定的 HTTP 200。

适用边界

本契约适用于百积木拥有的非流式 HTTP API 和 Module 方法响应。

以下边界保留各自的协议 envelope,并由最外层 Adapter 与 CModel 做一次确定性转换:

  • 健康检查和就绪探针;
  • SSE、WebSocket 等已经发出响应头的流式协议;
  • MCP、OpenAI、Anthropic 和第三方供应商协议;
  • 浏览器重定向、文件下载等非 JSON 响应。

外部错误格式不能继续进入百积木内部业务链路。

成功响应

{
  "contractVersion": "1.0.0",
  "errorCode": "0",
  "data": {
    "id": "example"
  }
}

成功响应必须同时满足:

  • HTTP 状态为 200;
  • contractVersion 缺省时按 1.0.0 解析;显式提供时必须为严格 SemVer 1.0.0;
  • errorCode 为字符串 "0";
  • data 由具体 API 或 Runtime 方法的返回类型定义。

使用 CModel envelope 的接口不能返回 204 No Content。

失败响应

{
  "contractVersion": "1.0.0",
  "errorCode": "PAYMENT_REQUIRED",
  "data": {
    "message": "当前账户余额不足,请充值后重试",
    "retryable": false
  }
}

失败响应必须同时满足:

  • HTTP 状态为 200;
  • errorCode 是非 "0" 的稳定字符串错误码;
  • data 可以为 null,或包含必填 message 与 retryable 的标准公开错误详情对象;
  • message 是 1 到 512 个字符、可直接展示给用户的安全文案;retryable 表示在不改变输入、授权或账户状态时,原操作能否通过重试成功。

业务失败必须使用 HTTP 200。HTTP 只表示 CModel envelope 已被正常传输,业务成败只由 errorCode 表达:"0" 为成功,非 "0" 为失败。网关、代理或容器在 CModel Handler 之外产生的非 200 响应属于传输或基础设施失败,不得伪装成 CModel 业务响应。

Envelope 与业务数据边界

CModel envelope 解析器只读取并校验三个已知字段:

  • contractVersion
  • errorCode
  • data

解析器必须忽略其他顶层字段。旧响应中残留的 value、systemCurrentTime、error 及其子字段都按未知顶层字段处理,不能导致协议解析失败,也不能继续作为业务判断或用户展示来源。为兼容协议版本字段引入前的 1.0.0 响应,消费者在 contractVersion 缺失时必须补成 1.0.0;一旦字段存在,非法 SemVer 或非 1.0.0 版本仍是协议错误。新生产者只能输出三个标准字段,并显式输出 contractVersion。

成功响应 data 的内部结构不由 CModel envelope 校验,具体 API、Module 方法或 Runtime methodDefinition 负责定义和校验。失败响应 data 由 CModel 统一定义为 null 或标准公开错误详情;消费者必须校验 message 和 retryable,并原样保留对象中的未知属性,以允许具体 API 在 1.0.0 中追加已文档化的公开操作信息。未知属性不能用于错误分类;分类仍只依赖该 API 登记的稳定 errorCode。

服务级错误传播路径

失败详情可携带可选的 data.errorPath,表示当前错误返回时经过的逻辑服务,按外层服务到错误源排列。 它是非空字符串数组,每个标识非空且不含空白或控制字符;允许真实的重复服务调用。 只有相应 API 已登记可公开的逻辑服务标识才能对外输出,内部服务拓扑不因该字段自动成为公共契约。

{
  "contractVersion": "1.0.0",
  "errorCode": "PAYMENT_REQUIRED",
  "data": {
    "message": "当前账户余额不足,请充值后重试",
    "retryable": false,
    "errorPath": ["checkout", "payment"]
  }
}

示例标识仅用于解释排列顺序,不是平台服务目录。

平台 Runtime 和模块 API 登记以下公开诊断标识;它们表示逻辑职责,不包含部署地址或实例身份:

逻辑服务标识职责
runtimeRuntime 请求执行入口
runtime-assemblyRuntime 装配与 Bundle 操作
module模块定义与版本

例如 errorPath: ["runtime", "runtime-assembly", "module"] 表示错误由模块服务返回,经过 Runtime 装配服务和执行入口传出。路径仅包含这次错误实际经过且已接入的服务,不保证覆盖尚未接入的服务;部署副本数不增加路径层级。服务身份由版本化配置登记,公开标识的新增或变化须同步 API 文档。

错误源初始化路径,每个上游服务由统一 SDK/响应出口 在数组头部追加自己的配置身份,一次服务调用只追加一次。客户端解析只校验并保留路径,不追加当前服务, 不修改原始 errorCode、message、retryable 或其他业务详情,也不把路径拼进 message。

服务内的方法栈、源码行号、实例地址和语言级 stacktrace 不写入路径;完整诊断通过追踪上下文关联内部日志。 该路径只描述返回的这次错误,不是完整调用图,不能用于鉴权、错误分类或重试决策。

该可选扩展保持 1.0.0 及原有必填字段:data: null 仍合法,不能改写成仅含 errorPath 的对象。 新接入的错误生产者应提供完整 message、retryable 及路径;上游不得为了补路径编造缺失的错误详情或 未收到的下游路径。成功响应的业务 data 不受此诊断字段约束,也不自动追加路径。

公网 Adapter 对未登记为公开诊断的内部路径执行移除,或按明确的 API 契约映射为公开逻辑服务标识。 映射不依赖消息文本,外部输入的路径不能冒充可信内部服务路径。

错误身份与公开边界

平台内部错误目录可以继续保存错误所有者、原因、分类、重试语义、用户文案和诊断索引,但公共调用方只依赖当前 API 文档登记的:

  • errorCode
  • 该 API 对此错误的稳定说明
  • data.message,仅用于用户展示
  • data.retryable,仅用于决定是否自动重试

最外层 Adapter 负责把内部错误确定性映射成 errorCode,只把已经审查为可公开展示的用户文案和稳定重试语义写入失败 data,并把有效 CModel envelope 统一输出为 HTTP 200。原始上游错误消息、数据库错误、堆栈、Token、Secret、内部域名、完整请求体和个人数据不能进入公共响应。

推荐实践:内部保留,公开边界显式映射

错误传播分为两个责任边界:

边界默认行为允许的转换
可信内部服务调用保留合法 CModel 的完整 errorCode、失败详情和重试语义;不要求调用方认识每个错误码增补当前服务的诊断路径;存在明确业务语义变化时包装,并保留原始原因
面向浏览器、外部集成的系统边界未登记来源错误统一投影为该 API 的安全默认错误仅按完整来源错误码做经审查的公开映射,独立确定公开说明与重试语义

内部默认保留不等于盲目转发字节:仍须验证 CModel 合同、请求身份和访问权限。协议无效、网络失败与合法业务失败分别处理,不把解析失败当成成功或空数据。内部扩展字段可能包含敏感数据,不能因此直接向浏览器输出。

公开映射是 API 合同的一部分,由 API Owner 登记并测试。多个来源错误可以映射成同一公开错误;需要不同用户操作的错误应分别映射,例如“重新登录”和“联系管理员检查调用上下文”不能都变成“浏览器打开失败”。不要按错误码后缀、HTTP 状态或消息关键词猜测,也不要因为下游新增错误码就自动公开。

公共 CModel 包从 10.2.0 提供显式边界策略。Rust 使用 baijimu-cmodel-http::PublicErrorPolicy,TypeScript 使用 @baijimu/cmodel 的 PublicErrorPolicy。原有内部解析接口继续保留未知错误;只有最外层 Adapter 选择公开策略。

use baijimu_cmodel_http::{PublicErrorDefinition, PublicErrorPolicy, RequestContext};

let policy = PublicErrorPolicy::new(
    PublicErrorDefinition::new("APP_SERVICE_UNAVAILABLE", "服务暂不可用,请提供请求 ID 联系管理员。", false)?,
    [("AUTH_REQUIRED".to_owned(),
      PublicErrorDefinition::new("APP_REAUTH_REQUIRED", "请重新登录。", false)?)],
)?;
// request_id 来自当前可信请求上下文;不是凭据或任意用户文本。
let context = RequestContext::new(request_id, None::<String>);
let public_failure = policy.project(&original_failure, &context)?;
// original_failure 仍在内部,public_failure 不复制它的 message 或扩展字段。

公开结果仅包含登记的错误码、审查过的说明、重试标志和当前关联 ID:

{
  "contractVersion": "1.0.0",
  "errorCode": "APP_REAUTH_REQUIRED",
  "data": {
    "message": "请重新登录。",
    "retryable": false,
    "requestId": "request-123"
  }
}

原始原因与请求关联

入口建立或验证 requestId,跨内部调用继续传递;已有可信 traceId 时一并关联。包装错误不能丢失原始错误码和来源请求 ID,也不能用后来查询告警的请求 ID 冒充原失败请求。

内部日志记录原始原因链的稳定标识、操作阶段与关联 ID;涉及凭据、个人数据的内容必须脱敏或排除。公开失败和成功 DTO 中的历史告警使用同一公开策略,不能通过 details、cause、lastError、审计接口或成功响应绕过边界。排障按关联 ID 查询受控日志,参见环境日志查询与错误排查。

边界测试至少覆盖:已登记映射、未知来源默认收敛、内部未知码保留、原始原因不被覆盖、请求 ID 贯通、并发请求上下文隔离,以及原始消息和扩展字段不泄露。公开映射变更按该 API 的版本合同评估兼容性,不因复用公共包而自动升级其他消费者。

HTTP 消费规则

调用方收到有效 CModel envelope 后,必须解析 envelope 并以 errorCode 判断业务结果:

  1. HTTP 200 可以承载成功或业务失败;errorCode == "0" 为成功,非 "0" 为失败。
  2. 失败响应的 data 可以是 null 或标准 { message, retryable } 对象;null 兼容已有生产者。
  3. contractVersion 缺失时按 1.0.0 处理;显式值非法或不受支持时报告协议错误,不做版本协商。
  4. 不接受数字 0、error_code、顶层 code/message 或缺失 errorCode。
  5. 忽略所有未知顶层字段;成功 data 交给具体接口的类型解析器,失败 data 交给 CModel 标准错误详情解析器。标准解析器校验 message、retryable 并保留端点扩展字段,但不得用扩展字段猜测错误类别。
  6. 错误分类只使用 errorCode 和当前 API 文档;不得用 HTTP 状态、value、data.message 或上游自然语言文本分类。

为支持已上线服务滚动迁移,SDK 1.1.4 的消费者可以解析历史非 200 失败 envelope,但仍只依据非零 errorCode 返回业务失败;所有新生产者和已迁移生产者必须输出 HTTP 200。当生产者盘点确认不再存在历史响应后,删除这项有明确退出条件的 consumer 兼容行为。

流式错误

流式协议不使用 CModel envelope。负责该流的协议必须定义自己的版本化错误事件,并在进入百积木非流式业务边界时由所属 Adapter 完成一次确定性转换。

不得把流内失败当作普通文本、成功数据或 JSON 解析异常吞掉。

收敛要求

生产者、消费者和 Gateway 必须直接收敛到同一个 1.0.0 三字段模型:

  1. 消费者只解析三个已知字段并忽略未知顶层字段;缺失的 contractVersion 统一规范化为 1.0.0。
  2. 生产者只序列化三个字段。
  3. Gateway 保留已经校验的错误码、原始消息和业务详情,仅按服务出口及公开可见性规则补充或移除 data.errorPath,对调用方统一输出 HTTP 200。
  4. 不增加双版本或版本协商;滚动迁移兼容只存在于 SDK consumer 边界,并以生产者全量迁移为删除条件。
  5. 生产者/消费者契约测试必须同时覆盖 HTTP 200 成功与失败、未知顶层字段、成功 data 类型所有权,以及失败 data 为 null 和标准错误详情两种形态。

本页内容