mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-02-02 16:31:11 +08:00
141 lines
5.1 KiB
Go
141 lines
5.1 KiB
Go
// 文件: 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, errors.New("Internal Panic Error"))
|
||
} 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
|
||
}
|