Appearance
Protocol: Errors
所有错误码定义。命名规则:E_<CATEGORY>_<SPECIFIC> 或 W_<CATEGORY>_<SPECIFIC>(警告)。
全部错误码(17 个)
权威定义见
packages/protocol/src/errors.ts的ERROR_META;category/retryable一律由该表推导,不接受调用方传入。每个码都有真实发射点(ADR-034)——
contract-coverage.test.ts静态扫描全仓库强制这条。 换句话说:这张表里的每一行,你写的分支都真的会被执行到。
播放期(player-core):
| Code | Category | Retryable | 说明 |
|---|---|---|---|
E_MANIFEST_PARSE | manifest | ❌ | manifest 解析失败 |
E_NETWORK | network | ✅ | 网络错误 |
E_NETWORK_TIMEOUT | network | ✅ | 网络超时 |
E_AUTH_EXPIRED | auth | ❌ | 401/403,签名 URL 过期或无权限(不做刷新,ADR-022) |
E_AUTOPLAY_BLOCKED | autoplay | ❌ | 浏览器拒绝自动播放 |
E_MEDIA_NOT_SUPPORTED | media | ❌ | 浏览器不支持此格式 |
E_MEDIA_DECODE | media | ✅ | 解码错误 |
E_MEDIA_ABORTED | media | ✅ | 播放被中止 |
E_SUBTITLE_LOAD_FAILED | media | ✅ | 字幕轨加载失败(不中断播放) |
E_DANMAKU_SEND_FAILED | env | ✅ | 弹幕发送失败(轨道满 / 参数不合法) |
E_METHOD_NOT_SUPPORTED | env | ❌ | 命令当前平台不支持(如运行时换内核) |
E_PLAYER_DESTROYED | env | ❌ | 命令打进了已销毁的播放器 —— 消费方自己调过 destroy()(ADR-067) |
E_INTERNAL | env | ❌ | SDK 内部错误 |
E_UNKNOWN | env | ❌ | 其他 |
iframe 传输层(frame-core):
| Code | Category | Retryable | 对应 FallbackReason |
|---|---|---|---|
E_LOAD_FAILED | network | ✅ | iframe_load_failed |
E_ENV_CSP_BLOCKED | env | ❌ | csp_denied |
E_HANDSHAKE_TIMEOUT | network | ✅ | handshake_timeout |
E_HANDSHAKE_VERSION_MISMATCH | env | ❌ | version_incompatible |
E_FRAME_CRASHED | env | ❌ | (无 —— 它不是加载期失败) |
iframe 降级走两条通道:
onFallback(reason)驱动兜底 UI,error事件进监控流。 前四个一一对应,见上表。
E_PLAYER_DESTROYED:唯一一个「消费方自己造成」的码
其余所有码描述的都是外部出了事(网络、媒体、环境、对端)。这一个不是: 只有消费方自己调过 destroy() 才到得了它。组件卸载走的是另一条路 —— 那时句柄已置 null,根本不下发命令(ADR-066)。
它填的是一个静默洞。 player-core 的每个命令开头都有 if (destroyed) return (坑 #3:destroy 之后不许再有事件出去),那个 return 是对的;错的是上一层从来不说话。 实测:消费方调完 destroy() 再改 source prop —— inline 两面 0 条事件, iframe 两面报 E_INTERNAL + 一句 penpal 的英文内部话。两半各错各的, 逐条数据在 archive/docs/specs/destroyed-player-command-signal.md § 1。
⚠️ 不要用 E_METHOD_NOT_SUPPORTED 代替它 —— 那是「这个方法在这里不支持」。 合成一个,监控里就再也分不开「调了个不支持的东西」和「你自己把它拆了」, 和下面 E_FRAME_CRASHED 拒绝并进 E_HANDSHAKE_TIMEOUT 是同一条理由。
静态 iframe 结构性没有这个码:embed-helper 的导出只有 on / off / destroy, 没有命令面(命令走 URL 参数),不存在「prop 变了 → 下发命令」这条路。
E_FRAME_CRASHED 和 E_HANDSHAKE_TIMEOUT 的分别
是运维含义的分别,不是时序细节:
| 什么时候 | 典型成因 | |
|---|---|---|
E_HANDSHAKE_TIMEOUT | 握手从未成功 | CDN 挂了 / URL 错了 / 版本目录不存在 / CSP |
E_FRAME_CRASHED | 握手成功之后失联 | 渲染进程崩溃 / OOM / iframe 被浏览器回收 |
合并成一个码,监控里就再也分不开这两类故障 —— 一个是「发布/配置出问题」, 一个是「运行时资源出问题」,处置动作完全不同。
E_FRAME_CRASHED 前面没有 FallbackReason:onFallback 服务的是加载期兜底 UI, 而崩溃发生在播放中。它伴随的信号是 playablechange { reason: 'frame_disconnected', recoverable: false } —— 那才是给 UI 的「露重试入口」结论(ADR-043 / ADR-054)。
平台差异(已在 e2e/parity/ 固化,不是漏实现): inline 永不产生(同上下文,崩了整页一起崩);静态 iframe 结构性没有(单向 postMessage, 宿主侧没有 SDK 代码去检测)。
::: 已删除的错误码 ADR-034 删掉了 12 个从未发射的码:E_MANIFEST_FETCH E_MANIFEST_UNSUPPORTEDE_NETWORK_CORS E_AUTH E_AUTH_FORBIDDEN E_ENV_UNSUPPORTED_CODECE_ENV_UNSUPPORTED_BROWSER E_ENV_PERMISSION_DENIED E_LOCALE_NOT_SUPPORTEDE_CONNECTION_LOST E_LOAD_TIMEOUT E_RECONNECT_FAILED。
如果你在旧文档 / 旧代码里见到它们:它们从来没有真正触发过,对应的分支是死代码。
⚠️ E_CONNECTION_LOST 是个例外,值得单独说:它当初描述的就是「iframe 连接断了」—— 和后来的 E_FRAME_CRASHED 是同一件事。删它的理由不是「这件事不重要」,而是 当时没有任何检测能力,留在契约里是谎言(ADR-034 的原话:「一个我们暂时检测不到的错误码, 留在契约里是谎言;而删掉之后将来能检测了再加回来,是向后兼容的 minor,代价极低」)。
ADR-054 补上了检测能力,于是它以 E_FRAME_CRASHED 这个更准确的名字回来了 —— 这正是 ADR-034 那条原则的兑现,不是反悔。 换名是因为 "connection lost" 和网络错误 (E_NETWORK)语义上撞车,而实际发生的是 iframe 那一侧没了。 :::
警告码
W_* 不中断播放,走 compatwarning 事件,和 error 是两条流。
| 警告码 | 含义 | 有发射点 |
|---|---|---|
W_BROWSER_INCOMPATIBLE | UC / 夸克等已知不兼容内核 | ✅ player-core/src/plugins/compat.ts |
W_VERSION_MISMATCH | 两侧契约同 major、minor 不同 | ✅ frame-core/src/create-frame.ts(握手后) |
W_EVENT_REJECTED | 收到一条过不了契约校验的入站事件,已丢弃 | ✅ frame-core/src/create-frame.ts(事件入口)· embed-helper/src/create-embed-listener.ts |
W_EVENT_REJECTED(ADR-087)携带 rejectedEvent —— 被丢那条事件的名字, 类型是 string 而不是 EventName:它装的恰恰是本地契约不认识的名字。
⚠️ 它最常见的成因不是「iframe 坏了」,是「iframe 比 host 新」。 ADR-070 允许 同 major 不同 minor 的两端通信,而「新增事件是向后兼容的 minor」这条在严格校验下 不再自动成立:4.3.0 的 iframe 新增的事件,4.1.0 的 host 不认识,于是被丢。 收到它先看两端版本差多少,再怀疑 iframe 有 bug。
⚠️ 同一个事件名每个播放器实例只报一次 —— 被拒的往往是 timeupdate 那种 4 次/秒的高频事件,不去重的话告警本身就是噪音。
⚠️ inline 两面永不产生它 —— 那两条路没有 wire,事件从 player-core 直接进消费方, 不存在「不认识的东西」。这是结构性差异,不是能力缺失。
W_VERSION_MISMATCH 的 payload 比另一个多两个可选字段 (hostVersion / iframeVersion)—— 可选是为了不动 W_BROWSER_INCOMPATIBLE 那一侧, 详见 ADR-070。ua 对它是无关信息,留必填是因为改可选对 TS 消费方是 breaking。
两个码的分工:
| 判定 | 后果 | |
|---|---|---|
E_HANDSHAKE_VERSION_MISMATCH(错误) | major 不同 → 不兼容 | 降级 UI + 句柄进 dead 态 |
W_VERSION_MISMATCH(警告) | 同 major、minor 不同 → 兼容 | 什么都不变,只报事实 |
⚠️ 它曾经是 ADR-034 漏掉的那一个,零发射点活了一个多月:那次清扫立的规则是 「错误码必须有发射点」,而它扫的是 ErrorCodeSchema,警告码住在 WarningCodeSchema 里, 整条没进扫描范围。现在两个 enum 都由 pnpm check:error-code-docs 双向钉着(#441)。
PlayerError 结构
ts
interface PlayerError {
code: ErrorCode // E_* 枚举(非自由字符串)
message: string
retryable: boolean // 从 ERROR_META 推导,不由调用方传
category: 'manifest' | 'media' | 'network' | 'auth' | 'env' | 'autoplay'
cause?: ErrorCause // 归一后的原始错误快照(debug 用)
}
interface ErrorCause {
name: string // 原始错误的 name;取不到时是 'Unknown'
message: string // 截断到 500 字符
code?: number // MediaError.code(1–4)等结构化码
status?: number // HTTP 状态码,区分 401 / 403
// 其余**原始值**字段(string / number / boolean)原样保留;嵌套对象一律不收
}cause 为什么是固定结构而不是 unknown(#270)
错误对象要跨 iframe,而 postMessage(Penpal 内部也是它)用的是结构化克隆。 makePlayerError 原先把原始错误对象直接塞进 cause,而 xgplayer 的错误上挂着 DOM MediaError —— 结构化克隆遇到它直接抛 DataCloneError,整条 envelope 发不出去。
后果不是「cause 丢了」,是消费方收到一条 code / category / retryable 全错的 E_INTERNAL (embed-app 的全局未捕获异常上报接住了那个 DataCloneError,把它包成了错误事件)。 inline 不过序列化,同一个失败给的是正确的 E_MEDIA_* —— cross-mode-parity 被打破。
⚠️ 这里此前写的是「跨 iframe 传输,所以必须可 JSON 序列化」。规矩对,前提错: JSON.stringify 遇到不可序列化的东西静默丢弃,structuredClone 抛异常, 两者对同一件事的处理正好相反。契约按 JSON 语义写、传输按克隆语义跑,于是没人执行。
现在 cause 由 makePlayerError 统一过 serializeCause(),字段全是原始类型, 结构化克隆不可能再抛。apps/embed-app 的发送出口另有一道 wireSafe 兜底 —— 它防的不是这个 bug(已根治),是下一个不可克隆字段。
cause 仍然只用于 debug,业务判断请用 code / category / retryable。
命令 rejection 的 code 跨 iframe 保真(#356)
命令失败(load() 跨内核、播放器未就绪……)不走 error 事件,走的是 RPC 的 rejection,和上面那条 envelope 是两条独立的路。它曾经有一条同类的漂移:
| 消费面 | 2026-08-26 之前 | 现在 |
|---|---|---|
| react / vue inline | E_METHOD_NOT_SUPPORTED | E_METHOD_NOT_SUPPORTED |
| react-frame / vue-frame | E_INTERNAL + category: 'env' | E_METHOD_NOT_SUPPORTED |
⚠️ 真因不是「penpal 过不去」,是载具选错了。 penpal 6.2.2 序列化 rejection 时 只对 returnValue instanceof Error 的值剥属性(只留 name / message / stack), 不是 Error 的值原样走结构化克隆,接收侧也不做还原。所以 embed-app 改抛普通对象 (command-rejection.ts),playerError 就跟着过来了。
class ... extends Error 不行 —— penpal 认的是 instanceof,继承一样命中。
宿主侧(frame-core/src/to-player-error.ts)对解包出来的 playerError 必须过 PlayerErrorSchema:它现在来自跨源 iframe,不再是本进程构造的,裸信任等于让对端 往消费方的错误分支里塞任意 code。校验不过就退回 E_INTERNAL。
四条消费面同一个期望,由 e2e/specs/integration/source-switch.spec.ts 在浏览器里钉住 (静态 iframe 那条路结构性没有命令回程,不参与这条)。
详细说明
权威来源:packages/protocol/src/errors.ts。另见 USER-GUIDE.md § 29 完整错误码表 + 用户建议。