判断 Poppy 草案里的错误码归属,先看它在草案文本中出现在哪类语句里:如果错误是被 MUST/MUST NOT/SHOULD(中文草案里的“必须/不得/应”)这类规范性表述约束的状态迁移或响应动作,就属于协议要求;如果它只出现在示例报文、附录枚举、某实现的说明或日志里,通常只是实现自定,不要反过来把它当成协议要求写进对外约定。传输层断连、超时、帧解析失败这类问题需要单独归为传输层问题,不要和协议错误放在同一个分支处理。
草案正文规范性语句中出现的错误描述属于协议约束,收到后应按草案语义处理并能在对外说明中声明;示例、附录、实现扩展里的码值属于实现自定,只在本端做映射。传输层故障不与协议错误混判。暂时无法归类的原始码值先进入待确认清单,用注入模拟错误的方式验证映射逻辑后再决定是否兼容,避免按猜测写死兼容分支。
在 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,且会话没有因为猜测式兼容被错误地继续下去。
用日志字段区分错误来源
只记录码值不足以定位问题,同一个码值出现在声明阶段和运行阶段,含义可能不同。日志至少覆盖下面这些字段,建议做成结构化输出,便于按类别和阶段筛选。
{
"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 映射表并注明草案条款位置;归入实现自定的,写在本端兼容层并标注适用范围;一直无法归类的,保留隔离动作,不要为了消除报错而放宽分支。回归验证可以保留当初的注入脚本或回放样本,在草案版本更新、对端升级后重跑一遍,确认分类结果没有变化,同时把清单变更留档,便于以后解释为什么某条码值这样处理。