DBX 后端异常处理与结构化错误码规范:从 Agent 契约到前端展示的端到端实战指南
【免费下载链接】dbx15MB,轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, cross-platform database client. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server and more.项目地址: https://gitcode.com/t8y2/dbx
本文以 DBX 仓库中已落地的错误处理体系为主线,系统讲解 Agent v2 类型化错误的解码、稳定错误码 catalog(
BackendErrorv1 envelope)、脱敏边界、Tauri/HTTP/多语句三种传输通道以及前端本地化展示与 Session/Runtime 恢复规则。读完本文,你将掌握如何在 DBX 中新增一个后端错误码、如何正确构造公共错误对象、如何编写恢复策略与前端翻译测试,以及为什么"从错误文本推断分类"是一条红线。
一、设计目标与整体分层
DBX 的后端错误处理体系解决三个核心问题:
- 让恢复逻辑依赖可验证的类型:恢复动作(保留 Session、隔离 Session、替换 Runtime)只由 Rust 侧的类型化事实驱动,绝不从错误文本猜测;
- 让对外错误身份稳定:每个错误都有永久保留、不可复用的
code与messageKey,错误码可以废弃但不能改义; - 保留经过公共边界脱敏的驱动诊断信息:数据库厂商返回的原始错误正文(如
relation does not exist、ORA-00942)可以透传到前端,但凭据、URL、Session 标识等敏感内容必须在公共边界被替换或删除。
文档明确说明:本文描述的是现有实现,不引入新的 Agent Protocol V3;结构化错误是 Agent Protocol v2 的可选 capabilitystructured_error_v1。
体系共分六层,各层职责单一(对应 docs/backend-error-handling.md):
| 层 | 职责 | 关键产物 |
|---|---|---|
| Agent | 只报告事实 | category、stage、operationOutcome、sessionDisposition及 JDBC 诊断字段 |
RustAgentCallError | 解码 Agent v2 结构化错误 | 类型化错误枚举(见 agent_driver.rs) |
RecoveryPolicy | 依据类型化错误与操作范围决策 | 保留/隔离 Session、替换 Runtime(见 agent_recovery.rs) |
BackendErrorcatalog | 映射稳定code/messageKey/白名单参数/安全诊断 | v1 envelope(见 backend_error.rs) |
| 查询层与传输边界 | 生成并携带公共错误对象,不重复分类 | QueryExecutionError::into_backend_error、Tauri/HTTP 适配 |
| 前端 | 本地化摘要与 detail 展示 | normalizeBackendError、translateBackendError |
这种分层带来的直接约束是:查询层、Tauri、HTTP 和多语句结果只负责携带BackendError对象,不重复做分类;分类的唯一入口在 Rust 侧。
二、Agent 调用契约与类型化入口
Agent runtime 必须完成 Protocol v2 handshake 并支持multi_session。调用方通过call_typed拿到类型化结果:
- 若 Agent 声明了
structured_error_v1,RPC 失败时返回AgentCallError::Structured; - 否则进入
Legacy兼容路径。
超时、取消、传输失败和契约不满足分别对应AgentCallError的Timeout、Canceled、Transport和ContractViolation变体(见 agent_driver.rs 附近的枚举定义)。
业务代码应使用类型化入口:
let result = client.call_typed::<Response>(method, params, timeout, cancel).await; if let Err(error) = &result { let decision = RecoveryPolicy::decide(error, RecoveryScope::UserOperation); // 只执行 Session/Runtime 恢复,不重放当前用户 SQL。 }AgentRuntimeClient::call和AgentCallError::into_legacy_string仅用于尚未迁移的字符串边界。旧字符串只有在try_agent_error_from_legacy能证明其来自 Agent 调用通道时才恢复为 Agent 错误;不要在query、schema、connection、keepalive或 UI 中新增任何文本分类规则——这是整个体系反复强调的红线,MySQL/SQL Server 的专用 batch executor 目前仍停留在字符串驱动边界,原因正是"驱动层尚未提供可验证的 typed failure facts"。
Agent 侧的类型化事实
Agent 上报的核心事实枚举(位于 agent_driver.rs)包括:
AgentSessionDisposition:Keep(保留)、Quarantine(隔离)、ReplaceRuntime(替换 Runtime);AgentErrorCategory:Connection、Sql、Resource、Protocol以及超时/取消等类别;AgentErrorStage:Request、Checkout、Connect、Validate、Execute、Fetch、Cancel、Close;AgentOperationOutcome:NotStarted(操作明确未开始)、Unknown(结果未知)。
这些枚举的组合关系(哪些 category/stage/outcome 组合是合法的)在AgentErrorContext与valid_agent_error_combination中定义,非法组合在 catalog 映射时会被归类为ContractInvalid(DBX-JDBC-5002),而不是被"翻译成"某个看似合理的错误码。
三、公共错误对象:BackendError v1 envelope
Rust 侧BackendError的字段全部保持私有,只能通过 catalog 构造器生成,目的是防止code、messageKey与参数声明三者漂移(见 backend_error.rs)。序列化到 JSON 时使用 camelCase:
{ "version": 1, "code": "DBX-JDBC-4001", "messageKey": "backendErrors.jdbc.sqlFailed", "messageParams": { "stage": "execute" }, "source": "jdbcAgent", "origin": { "subsystem": "database", "adapter": "native" }, "operationOutcome": "unknown", "detail": "relation missing_table does not exist", "errorPosition": { "line": 1, "column": 15, "offset": 14 }, "diagnostics": { "category": "sql", "stage": "execute", "sqlState": "42P01", "vendorCode": 0, "exceptionClass": "java.sql.SQLException" } }字段语义与约束
version:当前为1。新增可选字段可以保持 v1;改变已有字段的类型、必填性、语义或删除字段时必须升级版本。version表示 envelope 版本,不代表 Agent Protocol 版本。code/messageKey:发布后永久保留,不能复用或改义。废弃错误码只能停止新增使用,不能重新分配给其他含义;废弃时保留旧 locale 与兼容映射。source:v1 兼容字段,表示旧错误来源(jdbcAgent、jdbcAgentLegacy、legacyBackend);新代码改用origin描述子系统。客户端不能因为未知的 source/origin 值而丢弃整个 envelope。origin:可扩展元数据,至少包含subsystem(database、tunnel、extension、ai、messageQueue、backend)和adapter(jdbcAgent、native、plugin、http、legacy等),可选driver(例如 DuckDB 路径中为"duckdb")。它不参与错误分类、恢复或重试决策。diagnostics:白名单化的诊断字段。sqlState(最多 32 个可打印 ASCII 字符)、vendorCode、exceptionClass(最多 128 字符);diagnostics.adapterCode是适配器协议提供的可选出错码(例如 DuckDB worker 的duckdb_execute_failed、duckdb_worker_poisoned),仅用于诊断展示,不替代稳定的 DBXcode。errorPosition:适配器提供的可选出错位置,目前只有原生 PostgreSQL 驱动填充。line/column为 1-based、按 Unicode 码点计数,offset为语句内 0-based 码点下标,均相对实际下发的语句文本。它不参与分类、重试或恢复;客户端只有在能证明该语句仍映射回当前编辑器内容时才用它定位,否则忽略。位置以可解析后缀在db层内部传递,在query.rs还原为类型化字段,该后缀不会出现在detail或任何用户可见文本中。operationOutcome:只能是not_started或unknown。结果未知时不能自动重放用户操作。messageParams:只能包含 catalog 声明的 string、number、boolean 标量(对应 Rust 的BackendMessageParam::String/Integer/Boolean),不得携带 SQL、URL、凭据或任意对象。构造器内有debug_assert校验参数个数与类型合法性。- 字段私有性:Rust 字段保持私有,新增错误必须通过 catalog 构造,避免 code、key 和参数声明漂移。
错误码 catalog 全表
文档给出的 catalog 与源码 backend_error.rs 中的条目完全一致:
| code | 含义 |
|---|---|
DBX-JDBC-1001 | 连接建立失败 |
DBX-JDBC-1002 | 已建立连接中断 |
DBX-JDBC-2001 | 操作超时且尚未开始 |
DBX-JDBC-2002 | 操作超时但结果未知 |
DBX-JDBC-2003 | 操作取消 |
DBX-JDBC-3001 | 资源繁忙,操作尚未开始 |
DBX-JDBC-3002 | Runtime 被替换 |
DBX-JDBC-4001 | 数据库 SQL 执行失败 |
DBX-JDBC-5001 | Agent 传输或协议失败 |
DBX-JDBC-5002 | Agent 错误上下文违反契约 |
DBX-JDBC-9001 | 旧 Agent 错误无法可靠分类 |
DBX-LEGACY-0001 | 非 Agent 或未迁移的字符串错误 |
源码中还存在一个文档表格未列出的独立事务错误码:DBX-TXN-1001(backendErrors.transaction.sessionExpired,带timeoutSecs整数参数),由from_manual_transaction_session_expired生成,用于"手动事务会话已过期、DBX 已在执行前回滚"的场景。
分类映射的源码级细节
structured_entry函数把 Agent 的(category, stage, operationOutcome)组合映射到 catalog code(见 backend_error.rs),几个关键组合规则:
- Connection:
Request/Checkout/Connect/Validate阶段且NotStarted→DBX-JDBC-1001;Execute/Fetch/Cancel/Close阶段且Unknown→DBX-JDBC-1002;其余组合 →DBX-JDBC-5002。 - Timeout:
NotStarted→DBX-JDBC-2001;Unknown→DBX-JDBC-2002。 - Resource:
sessionDisposition == ReplaceRuntime→DBX-JDBC-3002;NotStarted→DBX-JDBC-3001;否则 →DBX-JDBC-5002。 - Sql:
Execute/Fetch/Cancel/Close阶段且Unknown→DBX-JDBC-4001;否则 →DBX-JDBC-5002。
也就是说,非法组合不会得到"看起来合理"的错误码,而是被明确标记为契约违例,这是"恢复逻辑依赖可验证类型"的底层保障。
新增错误码的标准流程
按文档要求,新增错误码需要四步:
- 在 backend_error.rs 的 catalog 中增加唯一
code、messageKey和参数声明(CatalogEntry+ParamSpec白名单); - 为所有 locale 增加相同 key(见 apps/desktop/src/i18n/locales 下的多语言文件),并扩展 catalog 完整性测试;
- 增加 Rust 映射和序列化测试,以及前端 normalize/翻译测试(见 backendErrors.spec.ts);
- 若错误来自 Agent,先在
AgentErrorContext中定义可验证的事实和合法组合,再添加 catalog 映射;不要用错误文本补分类。
四、detail 与安全边界
detail是数据库/驱动诊断的可选补充,不是分类依据。已类型化的 SQL 错误会保留数据库/驱动返回的原始正文;未知或连接类错误才走 DBX 的凭据与 Session 清洗兜底。
边界规则(文档原文要点)
- 大小上限:最多保留 64 KiB 的 UTF-8 文本(源码中
MAX_DETAIL_BYTES = 64 * 1024);超出部分按字符边界截断(bounded_text用char_indices保证不截断多字节字符),空内容丢弃。 - 换行语义:查询层补充上下文时使用独立换行符(
\n)追加,不以空格拼接;消费者与测试应保留该换行边界。 - 透传与脱敏:数据库厂商错误正文(
ERROR: relation ... does not exist、ORA-00942、约束冲突中的值、驱动返回的 statement 文本)原样保留;而连接配置和未知错误文本中的 JDBC URL、密码、token、授权头、密钥、Session 标识会被替换,或在只剩敏感内容时整个删除。 - 不解析 SQL:DBX 不解析、抽取或改写 SQL payload,也不会主动把执行 SQL 追加到错误——因此 SQL 方言、嵌套括号、引号和业务字面量不会被错误的通用字符串规则破坏。需要内部诊断时单独记录原始请求,不得把内部日志对象直接复用为公共 envelope。
- 内部字段不外泄:
AgentErrorContext中的agentSessionId、重试标记和内部恢复字段不会作为结构化字段进入公共 envelope;非 SQL 类别的驱动错误正文如果含 Session 或凭据文本,公共 detail 仍会脱敏。 without_detail()只在调用方明确要求隐藏 detail 时移除原文;超时和取消没有服务端 detail 时只返回摘要。
脱敏实现的源码佐证
backend_error.rs 中的脱敏管线清晰可循:
safe_detail:先脱敏,再判断"是否只剩敏感令牌"(contains_only_redacted_sensitive_tokens),只剩敏感内容时返回None;redact_sensitive_fragments:识别password=、token:等敏感键值对(sensitive_key_name覆盖 password、passwd、pwd、token、accessToken、refreshToken、secret、authorization、apiKey、credential、auth、key、user、username、uid、accessKey、privateKey、session、sessionId、agentSessionId、jwt、cookie 等),值替换为[redacted];bearer/authorization:后的令牌同样处理;支持引号包裹与花括号包裹的值、转义字符;redact_url_userinfo:扫描://后的 authority,把user:password@中的密码部分替换为[redacted];redact_session_identifier:匹配session id、session_id、agentSessionId及session:形式,替换随后的值;bounded_ascii用于sqlState、exceptionClass、adapterCode等诊断字段(仅保留可打印 ASCII 与空格并截断)。
不同来源的 detail 策略
| 场景 | code | detail 行为 | 实现入口 |
|---|---|---|---|
| 类型化 SQL 失败 | DBX-JDBC-4001 | 保留原生正文(bounded_native_detail,不重写) | from_sql_detail/from_sql_detail_with_position |
| 未知/连接类错误 | 视类型 | 脱敏兜底(bounded_detail→safe_detail) | from_agent_call_error的_分支 |
| 查询超时(Rust 执行器生成) | DBX-JDBC-2002(stageexecute) | 保留超时诊断 detail | from_timeout_detail |
PostgreSQL 原生ERROR:诊断 | DBX-JDBC-4001(stageexecute) | 保留原始 detail + 可选errorPosition | from_sql_detail_with_position;集成验证见 live_postgres_error_position.rs |
| DuckDB worker 错误 | DBX-JDBC-4001(SQL 类)或DBX-LEGACY-0001 | 保留Parser Error/Catalog Error等正文;worker code 进diagnostics.adapterCode | from_duckdb_worker_error |
| 连接/超时/取消/清理 | — | 不使用DBX-JDBC-4001分类 | — |
五、传输边界:Tauri、HTTP 与多语句查询
Tauri Desktop
查询命令将QueryExecutionError映射为BackendError。即使通过execute_multi命令执行单语句或事务查询,dbx-core也会在整个 multi-query 核心链路中保留QueryExecutionError,直到 Tauri 边界才转换为BackendError——不得先降级为字符串再重建 envelope。apps/desktop/src/lib/backend/tauri.ts 在查询失败时抛出BackendErrorException,前端因此能同时取得messageKey和原始detail。Tauri 的连接、传输、导入和导出边界也统一将拒绝结果转换为BackendErrorException;未知对象只提取有长度上限的message、reason或detail,内容为空时使用稳定摘要。
HTTP Web
crates/dbx-web 的 multi-query 路由消费 typed 核心入口,将AppError序列化为同一套 envelope;正常 HTTP 错误响应会保留按规则生成的detail。BackendError::without_detail()仅用于需要主动隐藏详情的兼容场景,不是默认响应路径。HTTP status 只表示传输结果,不能替代或改变BackendError.code。
桌面端 HTTP 失败(包括 multipart、SSE、上传、下载和 Nacos 特殊接口)必须调用backendResponseError,不能直接构造new Error(await response.text()),否则会丢失BackendError v1envelope。
多语句查询
ExecuteMultiResult.error和进度事件中的error是权威的结构化错误字段,execution_error表示该结果确实失败。已经进入 typed 通用逐语句路径的错误必须直接从QueryExecutionError生成该字段,不能从兼容字符串反向推断。MySQL 和 SQL Server 的专用 batch executor 当前仍是字符串驱动边界,只有在驱动层提供可验证的 typed failure facts 后才能迁移;旧的Error行仅用于兼容,真实查询结果中名为Error的普通列不能被当作失败。
六、前端展示规则
前端有两个核心函数(实现在 apps/desktop/src/i18n/backend-errors.ts):
normalizeBackendError:只接受完整且类型正确的 envelope;detail如果存在必须是 string,兼容 fallback 的单次上限为 64 KiB。解析嵌套的{ error }、{ backendError }、BackendErrorException和跨 realm 的 Error-like 对象时使用有限深度和循环检测;无法识别的对象只保留有界的message、reason或detail文本,空对象使用稳定摘要。translateBackendError的结构化路径为:- 使用
messageKey和messageParams生成当前 locale 的自定义摘要; - 若
detail非空且不同于摘要,在摘要后追加空行和 detail; - 无法识别的旧字符串继续原样展示或按兼容 pattern 翻译。
- 使用
catch 到异常时必须把原始对象传给翻译器:
translateBackendError(t, error)不要先执行error.message || String(error),否则会丢失messageKey、参数和服务端 detail。旧版非 i18n 页面可以使用formatError,但绝不能把结构化 envelope 直接转换成[object Object]。多语言 key 位于 apps/desktop/src/i18n/locales(如 en.ts、zh 等 locale 中的backendErrors.*),前端翻译与 normalize 行为由 backendErrors.spec.ts 覆盖。
七、协议演进与兼容规则
version表示 envelope 版本,不表示 Agent Protocol 版本。未知的大版本不能按旧字段强行解析;客户端应保留安全 fallback,并记录原始版本用于诊断。- 新增可选字段属于向后兼容变更;改变字段类型、必填性、枚举语义、错误码含义或安全边界时,必须发布新版本并保留旧版本适配器。
- 客户端应忽略未知的可选字段和未知的
source/origin枚举值,但仍严格校验version、code、messageKey、messageParams、operationOutcome和detail的基本类型。 code是稳定机器标识,不能复用;messageKey是稳定本地化标识,文案可以调整,但 key 的语义不能改变。错误码废弃时保留旧 locale 和兼容映射。- 结构化 envelope 可生成本地化摘要并追加按错误来源处理的
detail;旧字符串或 malformed object 使用有界文本 fallback;空响应只显示稳定摘要,不伪造数据库原因。 operationOutcome=unknown不能因为 fallback 文本、source、origin 或 detail 被推断为可重试;恢复决策只依赖 Rust 中的类型化事实。
兼容代码的退役门槛包括:不再存在直接读取 HTTP 响应文本并抛错的路径;迁移后的后端错误展示调用点不再在translateBackendError前预先提取.message/String(error);在所有消费者接受BackendError v1且线协议测试通过前,不移除旧字符串或旧版错误行。
八、恢复规则与 RecoveryPolicy 实现
恢复规则的核心原则是"结果未知不重放":
operationOutcome=unknown:禁止自动重放 SQL、写入、DDL、事务和批处理;- 用户操作:即使 Agent 声明可重试,也只做 Session/Runtime 恢复并向用户返回原错误;
- 只读 metadata:只有
connection + quarantine场景可以新建 Session 重试,最多一次; replace_runtime:移除共享同一 Runtime 的路由;最终决定权在 Rust,不在 Agent 或前端;- contract violation、timeout、cancel:至少隔离当前 Session;旧 Session 的迟到结果不得影响新的路由代际。
这些规则在 agent_recovery.rs 中有完全对应的实现。RecoveryScope区分UserOperation、ReadOnlyMetadata { retried }、Keepalive、ConnectionOpen;RecoveryDecision为KeepSession、RetryReadOnlyMetadata、QuarantineSession、ReplaceRuntime。RecoveryPolicy::decide的决策表如下:
| 错误变体 / 事实 | 决策 |
|---|---|
ContractViolation | QuarantineSession |
Transport | ReplaceRuntime |
Timeout/Canceled | QuarantineSession |
Structured且 category 为 Timeout/Canceled | QuarantineSession |
sessionDisposition = ReplaceRuntime | ReplaceRuntime |
sessionDisposition = Quarantine且 category=Connection 且ReadOnlyMetadata{retried:false} | RetryReadOnlyMetadata(仅此一处允许重试) |
sessionDisposition = Quarantine(其余) | QuarantineSession |
sessionDisposition = Keep或缺失 | KeepSession |
注意Keepalive与ConnectionOpen场景同样由类型化决策驱动,不因错误文本变化。恢复契约的自动化测试见 agent_recovery_contract.rs。
九、提交前检查清单
文档为涉及后端错误体系的改动提供了完整的本地验证命令(在仓库根目录执行):
cargo fmt --all -- --check cargo clippy -j 1 -p dbx-core --no-default-features --all-targets -- -D warnings cargo test -j 1 -p dbx-core --no-default-features --lib backend_error::tests cargo test -j 1 -p dbx-core --no-default-features --lib agent_recovery::tests cargo check -j 1 -p dbx-web --no-default-features pnpm typecheck pnpm vitest run apps/desktop/src/i18n/__tests__/backendErrors.spec.ts其中backend_error::tests与agent_recovery::tests分别覆盖 catalog 映射、脱敏边界与恢复决策(Rust 侧单测位于 backend_error.rs 与 agent_recovery.rs 的#[cfg(test)]模块),前端 vitest 覆盖normalizeBackendError/translateBackendError的解析与翻译路径。
十、实践要点速查
- 新错误先定类型事实,再定 code:来自 Agent 的错误先在
AgentErrorContext中定义合法的 category/stage/outcome/disposition 组合,非法组合自动落到DBX-JDBC-5002。 - 分类只发生在 Rust:任何
query/schema/connection/keepalive/UI 代码都不做文本分类;字符串只有经try_agent_error_from_legacy证明来源后才恢复为 Agent 错误。 - detail 双轨制:类型化 SQL 错误原样保留厂商正文(最多 64 KiB、按字符边界截断);未知与连接类错误走凭据/Session 脱敏,脱敏后只剩敏感令牌则整体丢弃。
- 前端必须传原始 error 对象:
translateBackendError(t, error),禁止先行error.message || String(error);HTTP 失败统一走backendResponseError。 operationOutcome=unknown永不重放:恢复决策只看 Rust 类型化事实;唯一允许的重试路径是只读 metadata 的connection + quarantine,且最多一次。version与code是永久契约:新增可选字段可保持 v1;改类型、必填性、语义或安全边界必须升版本并保留旧适配器;错误码只能废弃不能复用。
【免费下载链接】dbx15MB,轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, cross-platform database client. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server and more.项目地址: https://gitcode.com/t8y2/dbx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考