现象

Echo是golang语言开发的,高性能, 可扩展的微型web框架。

笔者在项目中一直使用Echo框架,一直是v3.x的版本,在项目达到一定阶段后,发现Echo已经发布了”v4”版本一段时间,于是想把框架升级成v4. 到目前为止,最新的版本是v4.11

升级之后,发现有些请求出现了问题,具体报错为:

"binding element must be a struct" introduced with path params binding

升级后编译一把过,说明API设计的兼容性没有问题,现在是运行时报错了。

整个demo来验证这个问题, 这个demo是典型的restful应用,url里的参数表示资源id,body传入数据:

package main

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "github.com/labstack/gommon/log"
)

func main() {
    s := echo.New()
    s.Use(middleware.Logger())

    s.PUT("/users/:id", func(c echo.Context) error {
        var data interface{}
        if err := c.Bind(&data); err != nil {
            log.Fatal(err)
        }
        log.Print(data)
        return nil
    })

    s.Start(":8811")
}

请求:

curl -X PUT -H "Content-Type: application/json" -d '{"name":"John"}' localhost:8811/users/1

例子很简单,有一个URL请求是POST /users/1, 传入的body是json {"name":"John"}.

这个例子造成Echo在调用c.Bind时报错。

我们看下Echo Bind的源码:

// Bind implements the `Binder#Bind` function.
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
        req := c.Request()

        names := c.ParamNames()
        values := c.ParamValues()
        params := map[string][]string{}
        for i, name := range names {
                params[name] = []string{values[i]}
        }
        if err := b.bindData(i, params, "param"); err != nil {
                return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
        }
        if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
                return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
        }
        if req.ContentLength == 0 {
                return
        }
        ctype := req.Header.Get(HeaderContentType)
        switch {
        case strings.HasPrefix(ctype, MIMEApplicationJSON):
                if err = json.NewDecoder(req.Body).Decode(i); err != nil {
                        if ute, ok := err.(*json.UnmarshalTypeError); ok {
                                return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)).SetInternal(err)
                        } else if se, ok := err.(*json.SyntaxError); ok {
                                return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err)
                        }
                        return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
                }    
         ....                     

可以看到,它先bindData params, 而这个param,即传入的”id”. 即Echo先尝试绑定param,在”ContentLength>0”时再绑定body。

和我们预想的使用c.Bind()来获取body数据不符。而查看”v3”版本的,是没有这个问题的。

解决方法

可以通过不同方法绕过c.Bind调用。

  1. 使用json.NewDecoder()或者json.Unmarshal()替换c.Bind
  2. echo支持自定义Binder对象,echo.Binder = MyBinder()

后记

笔者把该问题反馈给了上游, 发现有人已经提过issue, 我再补充一下。

作者之一貌似准备重新release一个版本。

我提出来是否可以用一个选项来开关该功能(param binding).