From 61e67bc2a034d587290557535ee56a98b500c15d Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:29:09 +0800 Subject: [PATCH 1/7] [context] fix writer reset --- context.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/context.go b/context.go index ff65ef3..7a64cc4 100644 --- a/context.go +++ b/context.go @@ -59,16 +59,8 @@ type Context struct { // reset 重置 Context 对象以供复用。 // 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化。 func (c *Context) reset(w http.ResponseWriter, req *http.Request) { - // 每次重置时,确保 Writer 包装的是最新的 http.ResponseWriter - // 并重置其内部状态 - if rw, ok := c.Writer.(*responseWriterImpl); ok { - rw.ResponseWriter = w - rw.status = 0 - rw.size = 0 - } else { - // 如果 c.Writer 不是 responseWriterImpl,重新创建 - c.Writer = newResponseWriter(w) - } + + c.Writer = newResponseWriter(w) c.Request = req c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组 From 0d6109f6da53a248fff0b82b2d497f0a9ab95c89 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:42:24 +0800 Subject: [PATCH 2/7] add adapter --- adapter.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 adapter.go diff --git a/adapter.go b/adapter.go new file mode 100644 index 0000000..68a2ddb --- /dev/null +++ b/adapter.go @@ -0,0 +1,53 @@ +// 文件: touka/adapter.go +package touka + +import ( + "net/http" +) + +// AdapterStdFunc 将一个标准的 http.HandlerFunc (func(http.ResponseWriter, *http.Request)) +// 适配成一个 Touka 框架的 HandlerFunc (func(*Context)) +// 这使得标准的 HTTP 处理器可以轻松地在 Touka 路由中使用 +// +// 示例: +// +// stdHandlerFunc := func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("Hello from a standard handler function!")) +// } +// r.GET("/std-func", touka.AdapterStdFunc(stdHandlerFunc)) +// +// 注意: 被适配的处理器执行完毕后,Touka 的处理链会被中止 (c.Abort()), +// 因为我们假设标准处理器已经完成了对请求的响应 +func AdapterStdFunc(f http.HandlerFunc) HandlerFunc { + return func(c *Context) { + // 从 Touka Context 中提取标准的 ResponseWriter 和 Request + // 并将它们传递给原始的 http.HandlerFunc + f(c.Writer, c.Request) + + // 中止 Touka 的处理链,防止执行后续的处理器 + c.Abort() + } +} + +// AdapterStdHandle 将一个实现了 http.Handler 接口的对象 +// 适配成一个 Touka 框架的 HandlerFunc (func(*Context)) +// 这使得像 http.FileServer, http.StripPrefix 或其他第三方库的 Handler +// 可以直接在 Touka 路由中使用 +// +// 示例: +// +// // 创建一个 http.FileServer +// fileServer := http.FileServer(http.Dir("./static")) +// // 将 FileServer 适配后用于 Touka 路由 +// r.GET("/static/*filepath", touka.AdapterStdHandle(http.StripPrefix("/static", fileServer))) +// +// 注意: 被适配的处理器执行完毕后,Touka 的处理链会被中止 (c.Abort()) +func AdapterStdHandle(h http.Handler) HandlerFunc { + return func(c *Context) { + // 调用 Handler 接口的 ServeHTTP 方法 + h.ServeHTTP(c.Writer, c.Request) + + // 中止 Touka 的处理链 + c.Abort() + } +} From 81fd3902cb98150d7f6017cd1d4f25a20f3dfbad Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:43:16 +0800 Subject: [PATCH 3/7] fix ctx lifetime --- engine.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/engine.go b/engine.go index 3c33e42..99f0608 100644 --- a/engine.go +++ b/engine.go @@ -331,6 +331,7 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (engine *Engine) handleRequest(c *Context) { httpMethod := c.Request.Method requestPath := c.Request.URL.Path + defer engine.pool.Put(c) // 查找对应的路由树的根节点 rootNode := engine.methodTrees.get(httpMethod) // 这里获取到的 rootNode 已经是 *node 类型 @@ -345,8 +346,8 @@ func (engine *Engine) handleRequest(c *Context) { if value.handlers != nil { //c.handlers = engine.combineHandlers(engine.globalHandlers, value.handlers) // 组合全局中间件和路由处理函数 c.handlers = value.handlers - c.Next() // 执行处理函数链 - c.Writer.Flush() // 确保所有缓冲的响应数据被发送 + c.Next() // 执行处理函数链 + //c.Writer.Flush() // 确保所有缓冲的响应数据被发送 return } @@ -400,8 +401,8 @@ func (engine *Engine) handleRequest(c *Context) { handlers = append(handlers, NotFound()) c.handlers = handlers - c.Next() // 执行处理函数链 - c.Writer.Flush() // 确保所有缓冲的响应数据被发送 + c.Next() // 执行处理函数链 + //c.Writer.Flush() // 确保所有缓冲的响应数据被发送 } // UnMatchFS HandleFunc From 740dce54a26215e27dedb52b0423b41d118010d8 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:43:49 +0800 Subject: [PATCH 4/7] optimize recovery --- recovery.go | 149 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/recovery.go b/recovery.go index 5597e53..fc1e573 100644 --- a/recovery.go +++ b/recovery.go @@ -1,38 +1,141 @@ +// 文件: touka/recovery.go package touka import ( - "fmt" + "errors" + "io" "log" + "net" "net/http" + "net/http/httputil" // 用于 DumpRequest + "os" "runtime/debug" + "strings" ) -// Recovery 返回一个 Touka 的 HandlerFunc,用于捕获处理链中的 panic。 -func Recovery() HandlerFunc { +// 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 和 recover() 来捕获 panic defer func() { if r := recover(); r != nil { - // 记录 panic 信息和堆栈追踪 - err := fmt.Errorf("panic occurred: %v", r) - log.Printf("[Recovery] %s\n%s", err, debug.Stack()) // 记录错误和堆栈 - - // 检查客户端是否已断开连接,如果已断开则不再尝试写入响应 - select { - case <-c.Request.Context().Done(): - log.Printf("[Recovery] Client disconnected, skipping response for panic: %v", r) - return // 客户端已断开,直接返回 - default: - // 客户端未断开,返回 500 Internal Server Error - // 使用统一的错误处理机制 - c.engine.errorHandle.handler(c, http.StatusInternalServerError) - // Abort() 确保后续的处理函数不再执行 - c.Abort() - } + // 捕获到 panic,调用配置的处理器 + handler(c, r) } }() - - // 继续执行处理链中的下一个处理函数 - c.Next() + 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 +} From 4249f0192ef19329b4b64cb7ec836ec47cf3fd53 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:44:45 +0800 Subject: [PATCH 5/7] add IsHijacked for respw && add recover for flush --- respw.go | 55 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/respw.go b/respw.go index 4821019..e94bfa7 100644 --- a/respw.go +++ b/respw.go @@ -3,13 +3,15 @@ package touka import ( "bufio" "errors" + "log" "net" "net/http" + "runtime/debug" ) // --- ResponseWriter 包装 --- -// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问。 +// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问 type ResponseWriter interface { http.ResponseWriter http.Hijacker // 支持 WebSocket 等 @@ -21,7 +23,7 @@ type ResponseWriter interface { IsHijacked() bool } -// responseWriterImpl 是 ResponseWriter 的具体实现。 +// responseWriterImpl 是 ResponseWriter 的具体实现 type responseWriterImpl struct { http.ResponseWriter size int @@ -29,7 +31,7 @@ type responseWriterImpl struct { hijacked bool } -// NewResponseWriter 创建并返回一个 responseWriterImpl 实例。 +// NewResponseWriter 创建并返回一个 responseWriterImpl 实例 func newResponseWriter(w http.ResponseWriter) ResponseWriter { rw := &responseWriterImpl{ ResponseWriter: w, @@ -40,6 +42,13 @@ func newResponseWriter(w http.ResponseWriter) ResponseWriter { return rw } +func (rw *responseWriterImpl) reset(w http.ResponseWriter) { + rw.ResponseWriter = w + rw.status = 0 + rw.size = 0 + rw.hijacked = false +} + func (rw *responseWriterImpl) WriteHeader(statusCode int) { if rw.hijacked { return @@ -56,7 +65,7 @@ func (rw *responseWriterImpl) Write(b []byte) (int, error) { } if rw.status == 0 { // 如果 WriteHeader 没被显式调用,Go 的 http server 会默认为 200 - // 我们在这里也将其标记为 200,因为即将写入数据。 + // 我们在这里也将其标记为 200,因为即将写入数据 rw.status = http.StatusOK // ResponseWriter.Write 会在第一次写入时自动调用 WriteHeader(http.StatusOK) // 所以不需要在这里显式调用 rw.ResponseWriter.WriteHeader(http.StatusOK) @@ -78,16 +87,42 @@ func (rw *responseWriterImpl) Written() bool { return rw.status != 0 } -// Hijack 实现 http.Hijacker 接口。 +// Hijack 实现 http.Hijacker 接口 func (rw *responseWriterImpl) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if hj, ok := rw.ResponseWriter.(http.Hijacker); ok { - return hj.Hijack() + // 检查是否已劫持 + if rw.hijacked { + return nil, nil, errors.New("http: connection already hijacked") } - return nil, nil, errors.New("http.Hijacker interface not supported") + + // 尝试从底层 ResponseWriter 获取 Hijacker 接口 + hj, ok := rw.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, errors.New("http.Hijacker interface not supported") + } + + // 调用底层的 Hijack 方法 + conn, brw, err := hj.Hijack() + if err != nil { + // 如果劫持失败,返回错误 + return nil, nil, err + } + + // 如果劫持成功,更新内部状态 + rw.hijacked = true + + return conn, brw, nil } -// Flush 实现 http.Flusher 接口。 +// Flush 实现 http.Flusher 接口 func (rw *responseWriterImpl) Flush() { + defer func() { + if r := recover(); r != nil { + // 记录捕获到的 panic 信息,这表明底层连接可能已经关闭或失效 + // 使用 log.Printf 记录,并包含堆栈信息,便于调试 + log.Printf("Recovered from panic during responseWriterImpl.Flush for request: %v\nStack: %s", r, debug.Stack()) + // 捕获后,不继续传播 panic,允许请求的 goroutine 优雅退出 + } + }() if rw.hijacked { return } @@ -96,7 +131,7 @@ func (rw *responseWriterImpl) Flush() { } } -// IsHijacked 方法返回连接是否已被劫持。 +// IsHijacked 方法返回连接是否已被劫持 func (rw *responseWriterImpl) IsHijacked() bool { return rw.hijacked } From 450d6294ad44c1f43c0c2fc8afbdef971c2f5840 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:13:17 +0800 Subject: [PATCH 6/7] remove dup engine.pool.Put(c) --- engine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/engine.go b/engine.go index 99f0608..337ebc6 100644 --- a/engine.go +++ b/engine.go @@ -331,7 +331,6 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (engine *Engine) handleRequest(c *Context) { httpMethod := c.Request.Method requestPath := c.Request.URL.Path - defer engine.pool.Put(c) // 查找对应的路由树的根节点 rootNode := engine.methodTrees.get(httpMethod) // 这里获取到的 rootNode 已经是 *node 类型 From 1d5ffac1533cc3ad1900720ddc341e0f9b317e63 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:40:40 +0800 Subject: [PATCH 7/7] [context] optimize context reset and reuse --- context.go | 7 ++++++- respw.go | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 7a64cc4..3172f6f 100644 --- a/context.go +++ b/context.go @@ -60,7 +60,12 @@ type Context struct { // 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化。 func (c *Context) reset(w http.ResponseWriter, req *http.Request) { - c.Writer = newResponseWriter(w) + if rw, ok := c.Writer.(*responseWriterImpl); ok && !rw.IsHijacked() { + rw.reset(w) + } else { + c.Writer = newResponseWriter(w) + } + //c.Writer = newResponseWriter(w) c.Request = req c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组 diff --git a/respw.go b/respw.go index e94bfa7..ebbc9ee 100644 --- a/respw.go +++ b/respw.go @@ -33,13 +33,12 @@ type responseWriterImpl struct { // NewResponseWriter 创建并返回一个 responseWriterImpl 实例 func newResponseWriter(w http.ResponseWriter) ResponseWriter { - rw := &responseWriterImpl{ + return &responseWriterImpl{ ResponseWriter: w, status: 0, // 明确初始状态 size: 0, hijacked: false, } - return rw } func (rw *responseWriterImpl) reset(w http.ResponseWriter) {