48 lines
3.2 KiB
Markdown
48 lines
3.2 KiB
Markdown
# 错误处理规范
|
|
|
|
> 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)。
|
|
|
|
## OCI 错误按语义分类,不要只看 HTTP 状态码
|
|
|
|
**问题**:OCI 同一状态码承载多种语义,按状态码一刀切会误判。已踩过的案例(GenAI 网关):
|
|
|
|
- **404 双义**:`NotAuthorizedOrNotFound`(消息 "Authorization failed or requested resource not found")是**租户级**无权限/无策略;"Entity with key … not found" 是**模型级**——该模型 OCID 在此区域无按需供给。前者应定论渠道无配额,后者应剔除该模型换下一个候选。
|
|
- **400 微调基座**:"Not allowed to call finetune base model …, use Endpoint: false" 表示模型在该区域仅作微调基座(dedicated 供给),同样是模型级不可用,不是请求参数错误。
|
|
- `ListModels` **没有字段**能事先区分 on-demand / dedicated 供给,只能在调用报错时习得;持久剔除依赖用户维护的模型黑名单(ai_model_blacklists,按模型名全局过滤,同步/探测不入库),错误消息应带模型名提示用户拉黑。
|
|
|
|
**约定**:识别特定语义一律走 `internal/oci/errors.go` 的判定函数(`IsModelUnavailable` / `IsEntityNotFound` / `IsOnDemandUnsupported`),用 `errors.As` 取 `common.ServiceError` 后按 状态码+消息片段 匹配;调用方(探测/网关路由)据此决定「定论、换候选、换渠道、是否计熔断」,不得在业务层散落字符串匹配。
|
|
|
|
## 外部响应体必须限长,超限报错而非静默截断
|
|
|
|
读上游/外部响应体一律 `io.LimitReader(max+1)` 再判长度:恰好读到 max+1 说明超限,返回带上限值的错误;不得截断后当成功继续(截断的 JSON/流式响应会以 200 返回坏数据,2026-07-22 审查 #13,genai_responses.go `readCompatBody`)。
|