touka/recovery.go
2025-06-06 21:43:49 +08:00

141 lines
5.1 KiB
Go
Raw Permalink 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.

// 文件: touka/recovery.go
package touka
import (
"errors"
"io"
"log"
"net"
"net/http"
"net/http/httputil" // 用于 DumpRequest
"os"
"runtime/debug"
"strings"
)
// PanicHandlerFunc 定义了用户自定义的 panic 处理函数类型
// 它接收当前的 Context 和 panic 的值
type PanicHandlerFunc func(c *Context, panicInfo interface{})
// RecoveryWithOptions 返回一个可配置的 panic 恢复中间件
//
// 参数:
// - handler (PanicHandlerFunc): 一个可选的回调函数 如果提供了,当 panic 发生时,
// 它将被调用,允许用户进行自定义的日志记录、错误上报或响应
// 如果为 nil将使用默认的 panic 处理逻辑
func RecoveryWithOptions(handler PanicHandlerFunc) HandlerFunc {
// 如果未提供 handler则使用默认的 panic 处理器
if handler == nil {
handler = defaultPanicHandler
}
return func(c *Context) {
defer func() {
if r := recover(); r != nil {
// 捕获到 panic调用配置的处理器
handler(c, r)
}
}()
c.Next() // 执行后续的处理链
}
}
// Recovery 返回一个使用默认配置的 panic 恢复中间件
// 它是 RecoveryWithOptions(nil) 的一个便捷包装
func Recovery() HandlerFunc {
return RecoveryWithOptions(nil) // 使用默认处理器
}
// defaultPanicHandler 是默认的 panic 处理逻辑
func defaultPanicHandler(c *Context, r interface{}) {
// 检查连接是否已由客户端关闭
// 常见的错误类型包括 net.OpError (其内部错误可能是 os.SyscallError)
// 以及在 HTTP/2 中可能出现的特定 stream 错误
// isBrokenPipeError 是一个辅助函数,用于检查这些情况
if isBrokenPipeError(r) {
// 如果是客户端断开连接导致的 panic我们不应再尝试写入响应
// 只需要记录一个信息级别的日志,然后中止处理
log.Printf("[Recovery] Client connection closed for request %s %s. Panic: %v. No response sent.",
c.Request.Method, c.Request.URL.Path, r)
c.Abort() // 仅设置中止标志
return
}
// 对于其他类型的 panic我们认为是服务器端内部错误
// 记录详细的错误日志,包括请求信息和堆栈跟踪
// 使用 httputil.DumpRequest 来获取请求的快照,但注意不要读取 Body
httpRequest, _ := httputil.DumpRequest(c.Request, false)
// 隐藏敏感头部信息,例如 Authorization
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.SplitN(header, ":", 2)
if len(current) > 1 && strings.EqualFold(current[0], "Authorization") {
headers[idx] = current[0] + ": [REDACTED]" // 替换为脱敏信息
}
}
redactedRequest := strings.Join(headers, "\r\n")
// 使用英文记录日志
log.Printf("[Recovery] Panic recovered:\nPanic: %v\nRequest:\n%s\nStack:\n%s",
r, redactedRequest, string(debug.Stack()))
// 在发送 500 错误响应之前,检查响应是否已经开始写入
// 如果 c.Writer.Written() 返回 true说明响应头已经发送
// 此时再尝试写入状态码或响应体会导致错误或 panic所以应该直接中止
if c.Writer.Written() {
// 使用英文记录日志
log.Println("[Recovery] Response headers already sent. Cannot write 500 error.")
c.Abort()
return
}
// 尝试发送 500 Internal Server Error 响应
// 使用框架提供的统一错误处理器(如果可用)
if c.engine != nil && c.engine.errorHandle.handler != nil {
c.engine.errorHandle.handler(c, http.StatusInternalServerError)
} else {
// 如果框架错误处理器不可用,提供一个备用的简单响应
// 返回英文错误信息
http.Error(c.Writer, "Internal Server Error", http.StatusInternalServerError)
}
// 确保 Touka 的处理链被中止
// errorHandle.handler 通常会调用 Abort但在这里再次调用是安全的
c.Abort()
}
// isBrokenPipeError 检查 recover() 捕获的值是否表示一个由客户端断开连接引起的网络错误
// 这对于防止在已关闭的连接上写入响应至关重要
func isBrokenPipeError(r interface{}) bool {
// 将 recover() 的结果转换为 error 类型
err, ok := r.(error)
if !ok {
return false // 如果 panic 的不是一个 error则不认为是 broken pipe
}
var opErr *net.OpError
// 检查错误链中是否存在 net.OpError
if errors.As(err, &opErr) {
var syscallErr *os.SyscallError
// 检查 net.OpError 的内部错误是否是 os.SyscallError
if errors.As(opErr.Err, &syscallErr) {
// 将系统调用错误转换为小写字符串进行检查
errMsg := strings.ToLower(syscallErr.Error())
// 常见的由客户端断开引起的错误消息
if strings.Contains(errMsg, "broken pipe") || strings.Contains(errMsg, "connection reset by peer") {
return true
}
}
}
// 还需要处理 HTTP/2 中的 stream closed 错误
// 在 Go 1.16+ 中,当写入已关闭的 HTTP/2 流时,可能会返回 io.EOF
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
// 在流式写入的上下文中io.EOF 或 net.ErrClosed 也常常表示连接已关闭
return true
}
if errors.Is(err, http.ErrAbortHandler) {
return true
}
return false
}