| name | error-chain-audit |
| description | 新增错误类型或错误传播路径、修改错误处理逻辑、新增异步任务(尤其是无人监听的任务)、修改错误日志输出时触发。 |
Skill: 错误传播链审计
在异步系统中,错误从产生点到最终处理点的传播链路比同步系统长得多。任何一个环节断裂——信息丢失、无人监听、上下文不足——都会导致故障无法定位、异常状态无法清理、或资源泄漏。本 skill 审计的是错误传播链路的完整性,而非特定的错误表示形式。
触发条件
- 新增错误类型或错误分类
- 新增或修改错误传播/转换/映射逻辑
- 新增异步任务启动(尤其是无人监听的任务)
- 修改错误日志输出
- 修改异常类型或异常处理逻辑
- 新增可能的失败路径
核心原理
错误传播的三个不变量
无论错误用枚举、异常、expected<T,E> 还是其他方式表示,三个不变量始终成立:
- 不丢失:错误从产生点到最终处理点的传播链中,任何环节都不得静默丢弃错误信息。丢弃错误等同于声明"此错误不可能发生"——如果这个声明不成立,就会产生不可诊断的 bug
- 不泄漏:错误信息在传播过程中不得泄漏到不该看到的接收方——包括审查系统(通过错误响应指纹)、日志系统中的敏感数据、或错误码枚举中的语义泄露
- 不终止:错误不得导致进程意外终止。每个无人监听的异步任务都必须保证内部错误被完整处理(记录 + 资源释放),不会冒泡到运行时触发进程崩溃
无人监听任务的特殊风险
异步系统中存在"发射后不管"的任务——启动者不等待其完成,也不接收其结果。这类任务中的错误没有外部接收者,如果不自行处理,错误会触发进程级终止。每个无人监听的任务都必须在内部完整处理所有可能的失败路径。
审计清单
1. 错误分类完整性
| 检查项 | 说明 |
|---|
| 每个错误可描述 | 系统中每种可发生的错误是否都有人类可读的描述。新增错误类型时是否同步更新描述。不可描述的错误在日志中表现为无意义的标识符,使排查无法进行 |
| 无死类型 | 是否存在定义了但从未产生的错误类型。死类型增加理解成本,且可能是遗漏的功能或残留的重构痕迹 |
| 无语义重叠 | 不同错误类型之间是否存在语义重叠。重叠会导致调用方无法区分具体错误,影响处理策略。如果两种错误总是被同一方式处理,考虑合并 |
| 底层错误不穿透 | 底层错误(操作系统、TLS 库、网络栈)是否被转换为业务语义的错误类型后传播,而非以原始形式穿透到上层。原始错误码对上层无意义,且会泄漏底层实现细节 |
2. 错误转换一致性
| 检查项 | 说明 |
|---|
| 转换覆盖完整 | 从底层错误到业务错误的转换是否覆盖了所有常见值。未转换的底层错误需要一个合理的兜底策略,而非静默忽略 |
| TLS 错误的特殊处理 | TLS 相关错误(连接截断、握手失败、解密失败、证书无效)是否有专门的转换。TLS 错误通常需要特殊处理(回落、重新握手),笼统转换会丢失处理机会 |
| 转换不丢信息 | 错误从一层转换到另一层时,是否保留了足够的原始信息供排查。如果转换后原始错误码丢失,排查时无法追溯到根因 |
| 转换方向明确 | 错误转换的方向是否一致——总是从底层到上层转换,而非混合方向。混合方向的转换链难以追踪 |
3. 无人监听任务的错误安全
| 检查项 | 说明 |
|---|
| 内部完整处理 | 每个无人监听的异步任务是否在内部捕获并处理所有可能的失败。必须同时处理已知错误类型和未知错误(兜底捕获),未知错误尤其需要记录类型信息 |
| 处理不丢信息 | 错误处理是否记录了足够的信息(类型、描述、发生位置)。静默处理(空处理块)等同于丢弃错误——如果错误不值得记录,那它是否值得处理 |
| 是否应改为有人监听 | 对于需要外部感知成功/失败的任务,是否应改为有人监听的模式。无人监听只适用于"真正的后台任务"(如日志刷入、指标聚合) |
| 资源释放 | 错误路径上是否正确释放了所有已获取的资源(连接、缓冲区、定时器)。错误处理不得跳过任何 RAII 析构 |
4. 错误到日志的映射
| 检查项 | 说明 |
|---|
| 日志级别合理 | 预期内的错误(如对端关闭、超时、取消)使用低级别;意外错误(如内存不足、内部状态不一致)使用高级别。过度使用高级别会导致运维疲劳,忽略真正严重的问题 |
| 上下文充足 | 日志中是否包含足够的上下文用于排查:操作类型、相关标识符、错误描述。仅有 "operation failed" 的日志无法定位问题。但也不要过度——日志不应包含完整调用栈 |
| 不泄漏敏感信息 | 错误日志是否遵循信息泄漏审计的要求——不包含密钥材料、不暴露实现特有的错误格式。参见 leak-audit |
| 调试日志的安全性 | 调试级别的日志是否可能包含敏感信息。调试日志在开发阶段有用,但生产构建中必须确保不输出。参见 leak-audit 日志安全节 |
5. 错误产生位置
| 检查项 | 说明 |
|---|
| 热路径不抛异常 | 数据转发、协议处理等热路径是否使用返回值传播错误,而非抛异常。异常在热路径中引入非确定性的性能抖动,且在异步环境中传播栈可能不准确 |
| 异常类型层次 | 自定义异常类型是否形成了合理的层次结构。基类提供通用信息,派生类提供领域特定信息。扁平的异常类型(每种错误一个独立类)增加维护成本 |
| 错误信息安全 | 错误的描述信息是否包含敏感数据(地址、域名、密钥)。错误信息可能被记录到日志或包含在错误响应中 |
审计流程
- 错误分类覆盖:列出系统中所有错误类型,逐一验证:是否有描述、是否在代码中产生、是否有调用方处理
- 转换链完整性:追踪错误从产生点到最终处理点的转换链,验证每步转换是否覆盖完整、是否丢信息
- 无人监听任务扫描:找出所有无人监听的异步任务,逐一验证:内部是否完整处理错误、处理是否记录信息、是否应改为有人监听
- 日志上下文审计:验证所有错误日志是否包含足够上下文、级别是否合理、是否安全
- 产生位置验证:验证热路径是否使用返回值、异常信息是否安全
常见反模式(禁止)
无人监听任务未处理错误
spawn_background_task(async_task);
spawn_background_task([]()
-> task<void>
{
try
{
co_await async_task();
}
catch (const std::exception& e)
{
log_warn("后台任务失败: {}", e.what());
}
catch (...)
{
log_error("后台任务失败: 未知错误");
}
});
日志缺少上下文
log_error("operation failed");
log_warn("connection closed");
log_warn("隧道关闭: {} -> {}, 入流量={}, 出流量={}, 原因={}",
client_addr, target_addr, bytes_in, bytes_out,
describe(error));
底层错误直接穿透
log_error("ssl error: {}", raw_platform_error_code);
log_error("TLS 握手失败: {} (详情: {})", describe(business_error), platform_error.message());
静默丢弃错误
try
{
co_await process_frame();
}
catch (...)
{
}
try
{
co_await process_frame();
}
catch (const std::exception& e)
{
log_warn("帧处理失败: {}", e.what());
}
热路径抛异常
auto plaintext = decrypt(ciphertext);
auto result = decrypt(ciphertext);
if (!result)
{
return result.error();
}
交叉引用
coroutine-audit 覆盖了异步任务的正确启动模式维度
co-lifecycle-audit 覆盖了错误路径上的对象析构和资源释放维度
leak-audit 覆盖了错误日志中的信息泄漏风险维度
crypto-audit 覆盖了密码学错误的特殊处理(AEAD 验证失败、nonce 耗尽)维度
enforce-coding Rule 13 定义了 fault::code vs exception 的使用边界,本 skill 审计传播链完整性
concurrency-audit 覆盖了 CAS 失败降级的错误处理、通道关闭后的错误传播维度
pool-audit 覆盖了 checkout 失败时的错误传播、健康检查失败的驱逐维度
tunnel-audit 覆盖了双向转发中的写入失败传播、所有退出路径的统计 flush 维度
mux-audit 覆盖了帧处理错误的协议错误帧发送、流异常退出通知维度