mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-02-03 08:51:11 +08:00
commit
8eeba0df72
5 changed files with 233 additions and 46 deletions
53
adapter.go
Normal file
53
adapter.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
11
context.go
11
context.go
|
|
@ -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 切片,而不是重新分配,以复用底层数组
|
||||||
|
|
|
||||||
|
|
@ -345,8 +345,8 @@ func (engine *Engine) handleRequest(c *Context) {
|
||||||
if value.handlers != nil {
|
if value.handlers != nil {
|
||||||
//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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,8 +400,8 @@ func (engine *Engine) handleRequest(c *Context) {
|
||||||
handlers = append(handlers, NotFound())
|
handlers = append(handlers, NotFound())
|
||||||
|
|
||||||
c.handlers = handlers
|
c.handlers = handlers
|
||||||
c.Next() // 执行处理函数链
|
c.Next() // 执行处理函数链
|
||||||
c.Writer.Flush() // 确保所有缓冲的响应数据被发送
|
//c.Writer.Flush() // 确保所有缓冲的响应数据被发送
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnMatchFS HandleFunc
|
// UnMatchFS HandleFunc
|
||||||
|
|
|
||||||
149
recovery.go
149
recovery.go
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
58
respw.go
58
respw.go
|
|
@ -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")
|
||||||
}
|
}
|
||||||
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() {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue