# 错误处理规范

> Go 后端错误处理约定。

- error 向上返回并用 `fmt.Errorf("...: %w", err)` 附加上下文,不吞错。
- 一个错误只处理一次:要么就地处理(重试、降级),要么包装返回;不要既打日志又返回。
- 包装消息描述当前操作(如 `load oci config`),不堆叠 `failed to` 前缀。
- 需要调用方区分的错误用 sentinel(`errors.Is`)或自定义类型(`errors.As`),不比对错误字符串。
- 正常流程不使用 `panic`;类型断言一律 `v, ok := x.(T)` 双返回值形式。

## 禁止:让错误消息携带敏感信息外流

**问题**:错误消息会流向后端日志与 HTTP 响应(respondError),其中可能嵌着凭据。典型案例:`http.Client.Do` 失败返回 `*url.Error`,其 `Error()` 拼出**完整请求 URL**——对 Telegram 这类把 token 放路径的 API(`/bot<token>/sendMessage`),token 直接进日志和接口响应。

```go
// 错误:url.Error 原样外传,token 泄漏
resp, err := c.Do(req)
if err != nil {
    return fmt.Errorf("send telegram message: %w", err)
}

// 正确:剥掉含 URL 的外壳,只保留底层原因(保持 %w 链)
if err != nil {
    return fmt.Errorf("send telegram message: %w", sanitizeURLError(err))
}
func sanitizeURLError(err error) error {
    var ue *url.Error
    if errors.As(err, &ue) { return ue.Err }
    return err
}
```

**预防**:凡外发 HTTP 且 URL/头中含凭据(token、签名、secret 路径段)的客户端,错误必须先脱敏再包装;新增此类客户端时补一条「错误不含凭据」的回归测试(参照 internal/service/notify_test.go 的 TestNotifierSendErrorHidesToken)。
