Files
xlgo-core/response/error.go
T
杭州明婳科技 6595e94677 fix: 多模型评审终审收口 — P0安全 + 主线A并发治理 + 文档对齐
经 deepseek/GLM/Claude 三轮对抗性评审 + 交叉核验 + 终审(见
last_report.md、cross_review_assessment.md),逐条回读源码核实并修复。
go build / vet / test -race ./... 全绿。

安全(P0):
- JWT alg confusion:WithValidMethods 固定 HMAC + keyfunc 方法断言
- JWT 空密钥 / 不支持算法 fail-closed(ErrEmptySecret / ErrUnsupportedAlgorithm)
- 上传大小实测封顶(enforceUploadSize),不信任客户端 file.Size
- HTTP headers/cookies map 竞态(写锁+快照)+ SSRF 防护(opt-in dialer.Control)

并发/资源(主线A 闭合):
- 包级可变全局一律 atomic.Pointer:DefaultRedis / DefaultStorage /
  DefaultManager / Validator / DefaultCache 等,消除裸指针数据竞争
- trace.Close 去 sync.Once(防 exporter 泄漏);app OnReady 失败走 Shutdown
- ws.Hub.Stop stopOnce 防 double-close;cron 接入 App 生命周期(WithCron)
- ratelimit failClosed atomic.Bool;Recover Written() 守卫
- cron checkAndRun 锁内收集锁外 spawn(wg.Add 在锁内防 Stop 竞态)

终审剩余项收口(H-A~M-H + L 系列):
- 副本连接池 MaxOpenConns/2 截断修复(replicaMaxOpenConns)
- JWT 黑名单 1s 超时 + 显式错误;ratelimit GetRedis 取一次复用
- GetPage 深分页上限;HashFile 流式;ReadFile 去 TOCTOU
- config Clone() 深拷贝;cache 锁操作返 ErrRedisNotReady
- compress 解压残留清理;正则预编译;EqualsIgnoreCase→EqualFold
- 删 redisLimiters 死代码;CLI 输入校验 + 回滚

文档:
- README/GUIDE/CHANGELOG 对齐当前 API(含破坏性变更迁移说明)
- docs/README 索引修正;config 注释修正(RS256 已不支持)

破坏性变更详见 CHANGELOG.md [Unreleased]。项目处于研发初期、无下游用户。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:16:03 +08:00

220 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package response
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 业务错误码定义
// 格式:模块(2位) + 功能(2位) + 错误类型(2位)
//
// 约定:
// - CodeSuccess = 0:成功
// - CodeFail = 1:通用失败
// - 其余 framework 内置错误码使用 HTTP 风格(401/403/404/429/500/503)或 5 位业务码段
// - 参数错误等业务化错误码请由业务项目自行定义,框架不再内置 CodeInvalidParams
const (
// 通用错误 00xxxx
CodeSuccess = 0 // 成功
CodeFail = 1 // 通用失败
CodeUnauthorized = 401 // 未授权
CodeForbidden = 403 // 无权限
CodeNotFound = 404 // 资源不存在
CodeRateLimit = 429 // 请求过于频繁
CodeServerError = 500 // 服务器错误
CodeServiceUnavailable = 503 // 服务不可用
// 用户模块错误 01xxxx
CodeUserNotFound = 10001 // 用户不存在
CodeUserAlreadyExists = 10002 // 用户已存在
CodeUserDisabled = 10003 // 用户已禁用
CodePasswordWrong = 10004 // 密码错误
CodePasswordWeak = 10005 // 密码强度不足
CodePhoneInvalid = 10006 // 手机号无效
CodeEmailInvalid = 10007 // 邮箱无效
CodeLoginFailed = 10008 // 登录失败
CodeTokenExpired = 10009 // Token 已过期
CodeTokenInvalid = 10010 // Token 无效
// 文件模块错误 02xxxx
CodeFileNotFound = 20001 // 文件不存在
CodeFileTooLarge = 20002 // 文件过大
CodeFileTypeInvalid = 20003 // 文件类型不支持
CodeFileUploadFailed = 20004 // 文件上传失败
// 数据模块错误 03xxxx
CodeDataNotFound = 30001 // 数据不存在
CodeDataAlreadyExists = 30002 // 数据已存在
CodeDataInvalid = 30003 // 数据无效
CodeDataConflict = 30004 // 数据冲突
// 业务模块错误 04xxxx
CodeOperationFailed = 40001 // 操作失败
CodeOperationTimeout = 40002 // 操作超时
CodeBusinessRuleError = 40003 // 业务规则错误
)
// Error 业务错误
type Error struct {
Code int // 错误码
Message string // 错误消息
Detail string // 详细信息(可选)
}
// NewError 创建业务错误
func NewError(code int, message string) *Error {
return &Error{
Code: code,
Message: message,
}
}
// NewErrorWithDetail 创建带详细信息的业务错误
func NewErrorWithDetail(code int, message, detail string) *Error {
return &Error{
Code: code,
Message: message,
Detail: detail,
}
}
// Error 实现 error 接口
func (e *Error) Error() string {
if e.Detail != "" {
return fmt.Sprintf("[%d] %s: %s", e.Code, e.Message, e.Detail)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// WithDetail 添加详细信息。
//
// H-10 修复:返回新 *Error 拷贝,不 mutate 接收者。预定义 Err*(如 ErrNotFound)是
// 包级共享指针,原实现 e.Detail = detail 会污染全局且并发调用存在数据竞争。
func (e *Error) WithDetail(detail string) *Error {
if e == nil {
return &Error{Detail: detail}
}
return &Error{
Code: e.Code,
Message: e.Message,
Detail: detail,
}
}
// ToResponse 转换为响应结构。
// 把 Detail 放入 data.detail(若非空),保留链路细节(M7:原实现丢 Detail)。
// P1 #15:仅当 detailExposed()(默认 trueApp 生产环境置 false)时才向客户端输出 Detail,
// 避免内部错误细节在生产泄露。
func (e *Error) ToResponse() Response {
if e.Detail != "" && detailExposed() {
return Response{
Code: e.Code,
Msg: e.Message,
Data: gin.H{"detail": e.Detail},
}
}
return Response{
Code: e.Code,
Msg: e.Message,
}
}
// 预定义错误
var (
ErrUnauthorized = NewError(CodeUnauthorized, "请先登录")
ErrForbidden = NewError(CodeForbidden, "无权限访问")
ErrNotFound = NewError(CodeNotFound, "资源不存在")
ErrRateLimit = NewError(CodeRateLimit, "请求过于频繁")
ErrServerError = NewError(CodeServerError, "服务器错误")
ErrServiceUnavailable = NewError(CodeServiceUnavailable, "服务暂时不可用")
// 用户相关
ErrUserNotFound = NewError(CodeUserNotFound, "用户不存在")
ErrUserAlreadyExists = NewError(CodeUserAlreadyExists, "用户已存在")
ErrUserDisabled = NewError(CodeUserDisabled, "用户已禁用")
ErrPasswordWrong = NewError(CodePasswordWrong, "密码错误")
ErrPasswordWeak = NewError(CodePasswordWeak, "密码强度不足")
ErrPhoneInvalid = NewError(CodePhoneInvalid, "手机号无效")
ErrEmailInvalid = NewError(CodeEmailInvalid, "邮箱无效")
ErrLoginFailed = NewError(CodeLoginFailed, "登录失败")
ErrTokenExpired = NewError(CodeTokenExpired, "登录已过期")
ErrTokenInvalid = NewError(CodeTokenInvalid, "Token 无效")
// 文件相关
ErrFileNotFound = NewError(CodeFileNotFound, "文件不存在")
ErrFileTooLarge = NewError(CodeFileTooLarge, "文件过大")
ErrFileTypeInvalid = NewError(CodeFileTypeInvalid, "文件类型不支持")
ErrFileUploadFailed = NewError(CodeFileUploadFailed, "文件上传失败")
// 数据相关
ErrDataNotFound = NewError(CodeDataNotFound, "数据不存在")
ErrDataAlreadyExists = NewError(CodeDataAlreadyExists, "数据已存在")
ErrDataInvalid = NewError(CodeDataInvalid, "数据无效")
ErrDataConflict = NewError(CodeDataConflict, "数据冲突")
// 业务相关
ErrOperationFailed = NewError(CodeOperationFailed, "操作失败")
ErrOperationTimeout = NewError(CodeOperationTimeout, "操作超时")
ErrBusinessRule = NewError(CodeBusinessRuleError, "业务规则错误")
)
// _errorCodeUniquenessGuard 在编译期保证所有内置 Code* 不重复。
// Go spec: 同一个常量 key 在 map 字面量中出现两次会触发
// "duplicate key in map literal" 编译错误,从而把"错误码不能撞"
// 这件事写进类型系统,比 init() 里 panic 检查更早、更安全。
//
// 维护规则:新增 Code* 常量时,**必须**把它登记到这里。
// 注意:变量名故意以下划线开头并赋给 _,避免 unused 报错且不会进入 runtime。
var _ = map[int]string{
CodeSuccess: "CodeSuccess",
CodeFail: "CodeFail",
CodeUnauthorized: "CodeUnauthorized",
CodeForbidden: "CodeForbidden",
CodeNotFound: "CodeNotFound",
CodeRateLimit: "CodeRateLimit",
CodeServerError: "CodeServerError",
CodeServiceUnavailable: "CodeServiceUnavailable",
CodeUserNotFound: "CodeUserNotFound",
CodeUserAlreadyExists: "CodeUserAlreadyExists",
CodeUserDisabled: "CodeUserDisabled",
CodePasswordWrong: "CodePasswordWrong",
CodePasswordWeak: "CodePasswordWeak",
CodePhoneInvalid: "CodePhoneInvalid",
CodeEmailInvalid: "CodeEmailInvalid",
CodeLoginFailed: "CodeLoginFailed",
CodeTokenExpired: "CodeTokenExpired",
CodeTokenInvalid: "CodeTokenInvalid",
CodeFileNotFound: "CodeFileNotFound",
CodeFileTooLarge: "CodeFileTooLarge",
CodeFileTypeInvalid: "CodeFileTypeInvalid",
CodeFileUploadFailed: "CodeFileUploadFailed",
CodeDataNotFound: "CodeDataNotFound",
CodeDataAlreadyExists: "CodeDataAlreadyExists",
CodeDataInvalid: "CodeDataInvalid",
CodeDataConflict: "CodeDataConflict",
CodeOperationFailed: "CodeOperationFailed",
CodeOperationTimeout: "CodeOperationTimeout",
CodeBusinessRuleError: "CodeBusinessRuleError",
}
// FailWithError 使用预定义错误响应(M7 修复:通过 ToResponse 保留 Detail)。
func FailWithError(c *gin.Context, err *Error) {
resp := err.ToResponse()
writeResp(c, resp.Code, resp.Msg, resp.Data)
}
// FailWithDetail 使用预定义错误并添加详细信息。
// P1 #15detail 仅在 detailExposed()(默认 trueApp 生产环境置 false)时输出给客户端。
func FailWithDetail(c *gin.Context, err *Error, detail string) {
if detailExposed() {
writeResp(c, err.Code, err.Message, gin.H{"detail": detail})
return
}
writeResp(c, err.Code, err.Message, nil)
}