Merge pull request #14 from infinite-iroha/dev

0.1.0
This commit is contained in:
WJQSERVER 2025-06-06 23:28:27 +08:00 committed by GitHub
commit 8eeba0df72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 233 additions and 46 deletions

53
adapter.go Normal file
View file

@ -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()
}
}

View file

@ -59,16 +59,13 @@ type Context struct {
// reset 重置 Context 对象以供复用。 // reset 重置 Context 对象以供复用。
// 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化。 // 每次从 sync.Pool 中获取 Context 后,都需要调用此方法进行初始化。
func (c *Context) reset(w http.ResponseWriter, req *http.Request) { func (c *Context) reset(w http.ResponseWriter, req *http.Request) {
// 每次重置时,确保 Writer 包装的是最新的 http.ResponseWriter
// 并重置其内部状态 if rw, ok := c.Writer.(*responseWriterImpl); ok && !rw.IsHijacked() {
if rw, ok := c.Writer.(*responseWriterImpl); ok { rw.reset(w)
rw.ResponseWriter = w
rw.status = 0
rw.size = 0
} else { } else {
// 如果 c.Writer 不是 responseWriterImpl重新创建
c.Writer = newResponseWriter(w) c.Writer = newResponseWriter(w)
} }
//c.Writer = newResponseWriter(w)
c.Request = req c.Request = req
c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组 c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组

View file

@ -346,7 +346,7 @@ func (engine *Engine) handleRequest(c *Context) {
//c.handlers = engine.combineHandlers(engine.globalHandlers, value.handlers) // 组合全局中间件和路由处理函数 //c.handlers = engine.combineHandlers(engine.globalHandlers, value.handlers) // 组合全局中间件和路由处理函数
c.handlers = value.handlers c.handlers = value.handlers
c.Next() // 执行处理函数链 c.Next() // 执行处理函数链
c.Writer.Flush() // 确保所有缓冲的响应数据被发送 //c.Writer.Flush() // 确保所有缓冲的响应数据被发送
return return
} }
@ -401,7 +401,7 @@ func (engine *Engine) handleRequest(c *Context) {
c.handlers = handlers c.handlers = handlers
c.Next() // 执行处理函数链 c.Next() // 执行处理函数链
c.Writer.Flush() // 确保所有缓冲的响应数据被发送 //c.Writer.Flush() // 确保所有缓冲的响应数据被发送
} }
// UnMatchFS HandleFunc // UnMatchFS HandleFunc

View file

@ -1,38 +1,141 @@
// 文件: touka/recovery.go
package touka package touka
import ( import (
"fmt" "errors"
"io"
"log" "log"
"net"
"net/http" "net/http"
"net/http/httputil" // 用于 DumpRequest
"os"
"runtime/debug" "runtime/debug"
"strings"
) )
// Recovery 返回一个 Touka 的 HandlerFunc用于捕获处理链中的 panic。 // PanicHandlerFunc 定义了用户自定义的 panic 处理函数类型
func Recovery() HandlerFunc { // 它接收当前的 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) { return func(c *Context) {
// 使用 defer 和 recover() 来捕获 panic
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
// 记录 panic 信息和堆栈追踪 // 捕获到 panic调用配置的处理器
err := fmt.Errorf("panic occurred: %v", r) handler(c, 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()
}
} }
}() }()
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
}

View file

@ -3,13 +3,15 @@ package touka
import ( import (
"bufio" "bufio"
"errors" "errors"
"log"
"net" "net"
"net/http" "net/http"
"runtime/debug"
) )
// --- ResponseWriter 包装 --- // --- ResponseWriter 包装 ---
// ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问 // ResponseWriter 接口扩展了 http.ResponseWriter 以提供对响应状态和大小的访问
type ResponseWriter interface { type ResponseWriter interface {
http.ResponseWriter http.ResponseWriter
http.Hijacker // 支持 WebSocket 等 http.Hijacker // 支持 WebSocket 等
@ -21,7 +23,7 @@ type ResponseWriter interface {
IsHijacked() bool IsHijacked() bool
} }
// responseWriterImpl 是 ResponseWriter 的具体实现 // responseWriterImpl 是 ResponseWriter 的具体实现
type responseWriterImpl struct { type responseWriterImpl struct {
http.ResponseWriter http.ResponseWriter
size int size int
@ -29,15 +31,21 @@ type responseWriterImpl struct {
hijacked bool hijacked bool
} }
// NewResponseWriter 创建并返回一个 responseWriterImpl 实例 // NewResponseWriter 创建并返回一个 responseWriterImpl 实例
func newResponseWriter(w http.ResponseWriter) ResponseWriter { func newResponseWriter(w http.ResponseWriter) ResponseWriter {
rw := &responseWriterImpl{ return &responseWriterImpl{
ResponseWriter: w, ResponseWriter: w,
status: 0, // 明确初始状态 status: 0, // 明确初始状态
size: 0, size: 0,
hijacked: false, hijacked: false,
} }
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) { func (rw *responseWriterImpl) WriteHeader(statusCode int) {
@ -56,7 +64,7 @@ func (rw *responseWriterImpl) Write(b []byte) (int, error) {
} }
if rw.status == 0 { if rw.status == 0 {
// 如果 WriteHeader 没被显式调用Go 的 http server 会默认为 200 // 如果 WriteHeader 没被显式调用Go 的 http server 会默认为 200
// 我们在这里也将其标记为 200因为即将写入数据 // 我们在这里也将其标记为 200因为即将写入数据
rw.status = http.StatusOK rw.status = http.StatusOK
// ResponseWriter.Write 会在第一次写入时自动调用 WriteHeader(http.StatusOK) // ResponseWriter.Write 会在第一次写入时自动调用 WriteHeader(http.StatusOK)
// 所以不需要在这里显式调用 rw.ResponseWriter.WriteHeader(http.StatusOK) // 所以不需要在这里显式调用 rw.ResponseWriter.WriteHeader(http.StatusOK)
@ -78,16 +86,42 @@ func (rw *responseWriterImpl) Written() bool {
return rw.status != 0 return rw.status != 0
} }
// Hijack 实现 http.Hijacker 接口 // Hijack 实现 http.Hijacker 接口
func (rw *responseWriterImpl) Hijack() (net.Conn, *bufio.ReadWriter, error) { 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")
} }
// 尝试从底层 ResponseWriter 获取 Hijacker 接口
hj, ok := rw.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("http.Hijacker interface not supported") 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() { 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 { if rw.hijacked {
return return
} }
@ -96,7 +130,7 @@ func (rw *responseWriterImpl) Flush() {
} }
} }
// IsHijacked 方法返回连接是否已被劫持 // IsHijacked 方法返回连接是否已被劫持
func (rw *responseWriterImpl) IsHijacked() bool { func (rw *responseWriterImpl) IsHijacked() bool {
return rw.hijacked return rw.hijacked
} }