Files
xlgo-core/middleware/recover.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

88 lines
3.1 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 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 500FailWithCode 经 writeResp→httpStatusFor
// 在默认 ModeBusiness 下对 CodeServerError 返回 200c.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()
}
}