gin排错记录 c.Next() c.Abrot()

[TOC]

记录一次哭笑不得的 gin 排错记录

代码

// 业务Error
type BError struct {
HTTPCode int `json:"-"` // Http 状态码
Code int `json:"code"` // 业务错误码
ErrMsg string `json:"error"` // 业务错误消息
ErrDetail string `json:"detail"`// 业务错误详情描述
}
// 有这么一个实例。http.StatusUnauthorized 就是 401
ErrToken = NewBError(http.StatusUnauthorized, 604, "token校验失败")

// 中间件 Auth
func (s *Server) Auth(c *gin.Context) {
// ...
// token = c.get(Authorization)
// token == "",终止并返回,提示重新登录
// !strings.Contains(token,"特定标记头 "),终止并返回,提示重新登录
// authStr 截取为 token 去掉特定标记头以后剩下的那一端,这才是 token 的有效值
// ...
tc, err := parseToken(authStr) // parseToken 负责调用库函数来解析 token。保证 tc、err 至少有一个为空。
if err != nil {
c.Abort()
c.JSON(ErrToken.HTTPCode, ErrToken)
return
}
_ = tc
c.Next()
}

// Gin Web Server 有很多接口,有的启用了中间件 Auth;有的不需要鉴权,就没启用 Auth。

Bug 表现

  • 同样都是确定启用了 Auth 的接口,
    • 比如一个 create 接口,在 token 不对的情况下,会返回业务错误信息,HttpStatusCode401
    • 而出问题的这个 config 接口(下文简称为问题接口),在 token 不对的情况下,虽然返回了业务错误信息,但是 HttpStatusCode 却是 200

相关技术介绍

  • gin 路由注册,收到客户端请求,在提供服务的时候,是链式响应的。

    • 每个 handler中间件都会被依次遍历到,逐个运行。
  • gin 最终处理请求的逻辑是在 func (engine *Engine) handleHTTPRequest(c *Context)

    func (engine *Engine) handleHTTPRequest(c *Context) {
    // ...
    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
    if t[i].method != httpMethod {
    continue
    }
    root := t[i].root
    // Find route in tree
    value := root.getValue(rPath, c.params, unescape)
    if value.params != nil {
    c.Params = *value.params
    }
    if value.handlers != nil {
    c.handlers = value.handlers
    c.fullPath = value.fullPath
    c.Next() //执行handlers
    c.writermem.WriteHeaderNow()
    return
    }
    // ...
    }
    break
    }
    // ...
    }
  • 可以看到在 engine 函数里,调用了一句 c.Next()

    // Next should be used only inside middleware.
    // It executes the pending handlers in the chain inside the calling handler.
    // See example in GitHub.
    func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
    c.handlers[c.index](c) //执行handler
    c.index++
    }
    }
  • Next() 的代码里,可以看到它会遍历执行每个 handler。当然,中间件也是 handler

  • 在官方注释中提到了两点:

    • Next() 应该仅在中间件内部被调用。—— 意思是不是中间件代码,就尽量别显式调用这个函数。
    • 它执行调用处理程序内部链中的挂起处理程序。—— 这句话描述了 Next() 在被调用以后会发生什么。
  • 不讲理论,我们直接举个例子来描述 c.Next() 的功效。

    • 如果现在有这样一个调用链
      • Handler1 —— print(1)
      • Handler2 —— print(2)
      • Handler3 —— print(3)
      • Handler4 —— print(4)
      • Handler5 —— print(5)
    • 正常执行,应该打印出 1 2 3 4 5
    • 但是如果,Handler2 的代码是这样的三句
      • print(A)
      • c.Next()
      • print(B)
    • 那么这时,会打印出 1 A 3 4 5 B —— 而不是 1 A B 3 4 5
  • c.Abort()c.Next() 都是用于调整 handlers 链的执行顺序的

// Abort prevents pending handlers from being called. Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates that the current request is authorized.
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
// for this request are not called.
func (c *Context) Abort() {
c.index = abortIndex
}
  • 举个例子:

    • 调用链和上面的一样
    • Handler2 代码如下,会打印出:1 A B
      • print(A)
      • c.Abort()
      • print(B)
    • Handler2 代码如下,会打印出:1 A B
      • print(A)
      • c.Abort()
      • c.Next()
      • print(B)
  • gin 中负责处理 Response Header 的函数在 $GINPATH/response_writer.go

  • w *responseWritergin.Context 的一个成员属性

    func (w *responseWriter) WriteHeader(code int) {
    if code > 0 && w.status != code {
    if w.Written() {
    debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
    }
    w.status = code
    }
    }

理论已知,开始实践

1、去除 c.Next()

了解了 c.Next() 的原理之后,我们应该意识到,原代码中 Auth 函数最后一行的 c.Next() 因为处于函数最后,所以留着也没用

2、c.w.WriteHeader() 测试

我们在下面代码中的A、B、C三个位置插入 c.w.WriteHeader()

每次仅保留一个位置有效,另外两个位置注释掉,执行 handler 过程,查看HTTP状态码是否符合期望。

func (s *Server) Auth(c *gin.Context) {
// ...
// token = c.get(Authorization)
// token == "",终止并返回,提示重新登录
// !strings.Contains(token,"特定标记头 "),终止并返回,提示重新登录
// authStr 截取为 token 去掉特定标记头以后剩下的那一端,这才是 token 的有效值
// ...
tc, err := parseToken(authStr) // parseToken 负责调用库函数来解析 token。保证 tc、err 至少有一个为空。
if err != nil {
c.w.WriteHeader(http.StatusUnauthorized) // A
c.Abort()
c.w.WriteHeader(http.StatusUnauthorized) // B
c.JSON(ErrToken.HTTPCode, ErrToken)
c.w.WriteHeader(http.StatusUnauthorized) // C
return
}
_ = tc
c.Next()
}

最终结论

  • 上文的实践中可以发现,c.JSON(ErrToken.HTTPCode, ErrToken) 会将HTTP状态码覆写为 200
  • 如果补充一个实验
    • 注释 A、B、C 三个位置
    • c.JSON(http.StatusUnauthorized, ErrToken) 代替 c.JSON(ErrToken.HTTPCode, ErrToken)
    • 实验结果将是,最终 http response 的HTTP状态码为 401
  • 经过深入查看源代码,发现,ErrToken.HTTPCode 为空值。ErrToken 构造函数里没赋值!