6595e94677
经 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>
88 lines
3.1 KiB
Go
88 lines
3.1 KiB
Go
package middleware
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"runtime/debug"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||
"github.com/EthanCodeCraft/xlgo-core/response"
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// Recover panic恢复中间件,捕获panic并返回统一错误响应
|
||
func Recover() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
defer func() {
|
||
if err := recover(); err != nil {
|
||
// 获取请求ID
|
||
requestID := GetRequestID(c)
|
||
|
||
// 记录错误日志
|
||
logger.Error("panic 已恢复",
|
||
zap.String("request_id", requestID),
|
||
zap.String("error", fmt.Sprintf("%v", err)),
|
||
zap.String("path", c.Request.URL.Path),
|
||
zap.String("method", c.Request.Method),
|
||
zap.String("stack", string(debug.Stack())),
|
||
)
|
||
|
||
// 返回错误响应。
|
||
// 必须用 response.Custom 显式写 HTTP 500:FailWithCode 经 writeResp→httpStatusFor,
|
||
// 在默认 ModeBusiness 下对 CodeServerError 返回 200,c.JSON(200,...) 会先 flush
|
||
// 锁定状态,随后 AbortWithStatus(500) 因 w.Written()==true 沦为 no-op,
|
||
// 导致客户端收到 HTTP 200 + body code:500,网关/APM 按 status 看不到 panic。
|
||
// Custom 不受 Mode 影响,直接写 500 且保留 RequestID。
|
||
//
|
||
// M-B 修复:若 panic 发生在响应已开始写出之后(流式/部分写),c.Writer.Written()==true,
|
||
// gin 拒绝再次写响应,response.Custom 沦为 no-op,客户端会收到截断响应而非 500。
|
||
// 此时不再尝试写 500(无效),仅 Abort + 记录,避免无意义的二次写入。
|
||
if !c.Writer.Written() {
|
||
response.Custom(c, http.StatusInternalServerError, response.CodeServerError, "服务器内部错误", nil)
|
||
} else {
|
||
logger.Warn("panic 发生在响应已开始写入后,无法写入 500,客户端将收到截断响应",
|
||
zap.String("request_id", requestID),
|
||
zap.String("path", c.Request.URL.Path),
|
||
zap.Int("bytes_written", c.Writer.Size()),
|
||
)
|
||
}
|
||
c.Abort()
|
||
}
|
||
}()
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// RecoverWithDetail panic恢复中间件(详细版本,返回更多信息)
|
||
// 注意: 生产环境不应使用,会暴露敏感信息
|
||
func RecoverWithDetail() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
defer func() {
|
||
if err := recover(); err != nil {
|
||
requestID := GetRequestID(c)
|
||
|
||
logger.Error("panic 已恢复",
|
||
zap.String("request_id", requestID),
|
||
zap.String("error", fmt.Sprintf("%v", err)),
|
||
zap.String("path", c.Request.URL.Path),
|
||
zap.String("method", c.Request.Method),
|
||
zap.String("stack", string(debug.Stack())),
|
||
)
|
||
|
||
// 开发环境返回详细错误。同样用 Custom 显式写 500(见 Recover 注释)。
|
||
// M-B 修复:响应已写出时不重复写 500(同 Recover)。
|
||
if !c.Writer.Written() {
|
||
response.Custom(c, http.StatusInternalServerError, response.CodeServerError, fmt.Sprintf("服务器内部错误: %v", err), nil)
|
||
} else {
|
||
logger.Warn("panic 发生在响应已开始写入后,无法写入 500,客户端将收到截断响应",
|
||
zap.String("request_id", requestID),
|
||
zap.String("path", c.Request.URL.Path),
|
||
)
|
||
}
|
||
c.Abort()
|
||
}
|
||
}()
|
||
c.Next()
|
||
}
|
||
} |