Poppy 草案里的错误码——哪些是协议约束哪些是实现自定?

文章导读
判断 Poppy 草案里的错误码归属,先看它在草案文本中出现在哪类语句里:如果错误是被 MUST/MUST NOT/SHOULD(中文草案里的“必须/不得/应”)这类规范性表述约束的状态迁移或响应动作,就属于协议要求;如果它只出现在示例报文、附录枚举、某实现的说明或日志里,通常只是实现自定,不要反过来把它当成协议要求写进对外约定。传输层断连、超时、帧解析失败这类问题需要单独归为传输层问题,不要和协
📋 目录
  1. 一 在 Poppy 草案里定位错误处理与状态码说明
  2. 二 把错误分类为协议约束、实现自定、传输层问题
  3. 三 用通用错误映射骨架处理三类错误
  4. 四 用日志字段区分错误来源
  5. 五 把未知错误码送入待确认清单
A A

判断 Poppy 草案里的错误码归属,先看它在草案文本中出现在哪类语句里:如果错误是被 MUST/MUST NOT/SHOULD(中文草案里的“必须/不得/应”)这类规范性表述约束的状态迁移或响应动作,就属于协议要求;如果它只出现在示例报文、附录枚举、某实现的说明或日志里,通常只是实现自定,不要反过来把它当成协议要求写进对外约定。传输层断连、超时、帧解析失败这类问题需要单独归为传输层问题,不要和协议错误放在同一个分支处理。

草案正文规范性语句中出现的错误描述属于协议约束,收到后应按草案语义处理并能在对外说明中声明;示例、附录、实现扩展里的码值属于实现自定,只在本端做映射。传输层故障不与协议错误混判。暂时无法归类的原始码值先进入待确认清单,用注入模拟错误的方式验证映射逻辑后再决定是否兼容,避免按猜测写死兼容分支。

在 Poppy 草案里定位错误处理与状态码说明

排查前先把草案当成一份可检索的文本,逐处确认错误描述的位置。常见位置有这几类,可以先都过一遍,再判断哪些具有约束力:

  • 错误处理或错误语义的独立章节:通常描述错误发生后对端和本端各自应做什么,规范性最强。
  • 状态机或状态迁移章节:错误被引用为前提条件时,说明它会影响后续必须发送什么,一般属于协议约束。
  • 消息格式定义中的错误字段与取值说明:要看取值是开放式还是封闭式枚举,以及草案是否要求注册或保留扩展空间。
  • 附录、示例报文、参考实现片断:多数只用于说明,不构成协议要求,需要结合上下文确认。
  • 草案的版本号与变更记录:不同版本的取值和语义可能变化,用旧版本码值去对新版本实现,容易得出错误结论。

判定信号建议记成几条可复核的问题:这段文字里有没有规范性关键词?这个错误有没有被状态迁移或响应动作引用?它是否出现在注册表或封闭枚举里?如果三条都不满足,只出现在示例和实现文档里,就先按实现自定处理,而不是协议约束。

把错误分类为协议约束、实现自定、传输层问题

把所有错误都当成协议要求,是排查跑偏最常见的原因:拿到一个未在草案里出现过的码值,就去猜它对应哪条规范,结果映射分支越加越多。可以先按三栏归类,再决定处理动作。

Poppy 草案里的错误码——哪些是协议约束哪些是实现自定?
类别典型来源判断问题处理方向
协议约束草案正文规范性语句、状态迁移、封闭枚举草案是否要求在此情形下必须终止会话或返回指定响应?按草案语义处理,不重试或按草案规定重试
实现自定示例报文、实现扩展、对端私有取值它是否只在该实现或该示例中出现,草案未定义其语义?本端做兼容映射,不写进对外契约
传输层问题连接断开、超时、帧或编码解析失败错误发生时连接是否还能正常收发?报错是否来自本端解析器而非对端消息?重建连接或直接失败,不与协议错误共用分支

三栏判定完,仍未落位的原始码值不要硬塞进任何一栏,先标为未知,交给后面的待确认流程。

用通用错误映射骨架处理三类错误

映射逻辑建议不绑定具体码值:码值与类别的对应关系放在配置或查表结构里,代码只按类别分支。这样草案更新或对端换实现时,改的是数据,不是控制流。下面是一段语言无关的骨架,可直接翻译成项目里常用的写法。

type Category = "protocol" | "impl" | "transport" | "unknown"

handle_error(raw, ctx):
    cat = classify(raw, ctx.stage, ctx.draft_version)   # 查表,不在代码里写死码值
    switch cat:
        case "protocol":
            action    = protocol_action(raw)            # 查草案映射表,查不到即降级为 unknown
            retryable = false
        case "impl":
            action    = "local-compat"
            retryable = is_transient(ctx)               # 如对端重启、限流窗口
        case "transport":
            action    = "reconnect-or-fail"
            retryable = true
        default:
            action    = "quarantine"
            retryable = false
            pending_list.add(raw, ctx)                  # 进待确认清单
    log(stage=ctx.stage, category=cat, raw=raw,
        action=action, retryable=retryable, draft=ctx.draft_version)
    return {category: cat, action: action, retryable: retryable}

需要替换的地方只有两处:classify 的数据源和 protocol_action 的映射表,两者都建议从配置加载并带上草案版本号。验证方式是注入模拟错误:构造一条携带未注册码值的响应,或让对端在协商阶段返回异常取值,观察日志里的 category 是否落到 unknown、action 是否为 quarantine,且会话没有因为猜测式兼容被错误地继续下去。

用日志字段区分错误来源

只记录码值不足以定位问题,同一个码值出现在声明阶段和运行阶段,含义可能不同。日志至少覆盖下面这些字段,建议做成结构化输出,便于按类别和阶段筛选。

Poppy 草案里的错误码——哪些是协议约束哪些是实现自定?
{
  "ts": "事件时间",
  "session_id": "会话标识",
  "stage": "declare | negotiate | run",
  "category": "protocol | impl | transport | unknown",
  "raw_code": "对端返回的原始码值,原样记录",
  "raw_message": "对端原始描述,截断长度要够用",
  "direction": "in | out",
  "draft_version": "草案版本标识",
  "action": "terminate | local-compat | reconnect-or-fail | quarantine",
  "retryable": "true | false",
  "peer": "对端实现与版本,能拿到就记"
}

阶段划分建议按声明、协商、运行三段:声明阶段的能力或版本不匹配类错误,多数属于协议约束;协商阶段的取值异常,需要看草案是否定义了该取值的处理;运行阶段的报错则要先排除传输层,再判断是否协议错误。把 stage 和 raw_code 一起记录,回查时才能判断是声明阶段就谈不拢,还是运行中途才出错。

把未知错误码送入待确认清单

未识别码值的默认动作是隔离,不是兼容。清单里每条记录至少包含:原始码值、首次出现阶段、对应草案版本、对端实现与版本、是否阻塞会话、当前临时处理方式、待确认问题(是否出现在规范性语句、是否在注册表内)、复核人和复核时间。

确认之后再迁移:归入协议约束的,补进 protocol_action 映射表并注明草案条款位置;归入实现自定的,写在本端兼容层并标注适用范围;一直无法归类的,保留隔离动作,不要为了消除报错而放宽分支。回归验证可以保留当初的注入脚本或回放样本,在草案版本更新、对端升级后重跑一遍,确认分类结果没有变化,同时把清单变更留档,便于以后解释为什么某条码值这样处理。