【问题标题】:Explicitly handle gzipped json before binding在绑定之前显式处理压缩的 json
【发布时间】:2021-12-20 09:16:39
【问题描述】:

我想编写一个 api,它将通过 POST 发送 gzip 压缩的 json 数据。虽然下面可以处理正文中的简单 json,但如果 json 被 gzip 压缩,则不处理。

在使用c.ShouldBindJSON之前是否需要显式处理解压?

如何重现

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
)

func main() {
    r := gin.Default()

    r.POST("/postgzip", func(c *gin.Context) {
        
        type PostData struct {
            Data string `binding:"required" json:"data"`
        }
        
        var postdata PostData
        if err := c.ShouldBindJSON(&postdata); err != nil {
            log.Println("Error parsing request body", "error", err)
            c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        log.Printf("%s", postdata)
        if !c.IsAborted() {
            c.String(200, postdata.Data)
        }
    })
    r.Run()
}
❯ echo '{"data" : "hello"}' | curl -X POST -H "Content-Type: application/json" -d @- localhost:8080/postgzip
hello

期望

$ echo '{"data" : "hello"}' | gzip | curl -v -i -X POST -H "Content-Type: application/json" -H "Content-Encoding: gzip" --data-binary @- localhost:8080/postgzip
hello

实际结果

$ echo '{"data" : "hello"}' | gzip | curl -v -i -X POST -H "Content-Type: application/json" -H "Content-Encoding: gzip" --data-binary @- localhost:8080/postgzip
{"error":"invalid character '\\x1f' looking for beginning of value"}

环境

  • 去版本:go version go1.17.2 darwin/amd64
  • gin 版本(或提交参考):v1.7.4
  • 操作系统:MacOS Monterey

【问题讨论】:

    标签: json go gzip go-gin


    【解决方案1】:

    在使用 c.ShouldBindJSON 之前是否需要显式处理解压?

    当然。 Gin ShouldBindJSON 不知道您的有效负载可能会或可能不会被编码。 It expects JSON input,顾名思义。

    如果你想编写可重用的代码,你可以实现Binding接口。

    一个非常简单的例子:

    type GzipJSONBinding struct {
    }
    
    func (b *GzipJSONBinding) Name() string {
        return "gzipjson"
    }
    
    func (b *GzipJSONBinding) Bind(req *http.Request, dst interface{}) error {
        r, err := gzip.NewReader(req.Body)
        if err != nil {
            return err
        }
        raw, err := io.ReadAll(r)
        if err != nil {
            return err
        }
        return json.Unmarshal(raw, dst)
    }
    

    然后可以与c.ShouldBindWith 一起使用,它允许使用任意绑定引擎:

    err := c.ShouldBindWith(&postData, &GzipJSONBinding{})
    

    【讨论】:

    • 打开 Content-Encoding 标头可能也是一个好主意,对吧?
    • @Moulick 是的,但不在绑定中。在调用 ShouldBindWith 之前,您应该检查处理程序中的标头
    • 那么 net/http 压缩的意义何在,它是否也不能处理某种类型的压缩,就像它实际上有一个 DisableCompression bool 一样? pkg.go.dev/net/http#Transport
    • @Moulick http.Transport 及其压缩选项与发出请求相关,即在 http client 中。在这里你正在实现一个服务器
    • 非常感谢。将来我会将完整的工作代码附加到此答案中
    【解决方案2】:

    开启Content-Encoding的完整工作示例

    curl 尝试使用它

    $ echo '{"data" : "hello"}' | gzip | curl -X POST -H "Content-Type: application/json" -H "Content-Encoding: gzip" --data-binary @- localhost:8080/json
    hello
    
    $ curl -X POST -H "Content-Type: application/json" --data-raw '{"data" : "hello"}' localhost:8080/json
    hello
    
    package main
    
    import (
        "bytes"
        "compress/gzip"
        "encoding/json"
        "fmt"
        "github.com/gin-gonic/gin"
        "github.com/gin-gonic/gin/binding"
        "io"
        "log"
        "net/http"
    )
    
    type PostData struct {
        Data string `binding:"required" json:"data"`
    }
    
    func main() {
        r := gin.Default()
        r.POST("/json", func(c *gin.Context) {
    
            var postdata PostData
    
            contentEncodingHeader := c.GetHeader("Content-Encoding")
            switch contentEncodingHeader {
            case "gzip":
                if err := c.ShouldBindBodyWith(&postdata, gzipJSONBinding{}); err != nil {
                    log.Println("Error parsing GZIP JSON request body", "error", err)
                    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                    return
                }
            case "":
                if err := c.ShouldBindJSON(&postdata); err != nil {
                    log.Println("Error parsing JSON request body", "error", err)
                    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                    return
                }
            default:
                log.Println("unsupported Content-Encoding")
                c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "unsupported Content-Encoding"})
                return
    
            }
    
            log.Printf("%s", postdata)
            if !c.IsAborted() {
                c.String(200, postdata.Data)
            }
        })
        r.Run()
    }
    
    type gzipJSONBinding struct{}
    
    func (gzipJSONBinding) Name() string {
        return "gzipjson"
    }
    
    func (gzipJSONBinding) Bind(req *http.Request, obj interface{}) error {
        if req == nil || req.Body == nil {
            return fmt.Errorf("invalid request")
        }
        r, err := gzip.NewReader(req.Body)
        if err != nil {
            return err
        }
        raw, err := io.ReadAll(r)
        if err != nil {
            return err
        }
        return json.Unmarshal(raw, obj)
    }
    
    func (gzipJSONBinding) BindBody(body []byte, obj interface{}) error {
        r, err := gzip.NewReader(bytes.NewReader(body))
        if err != nil {
            return err
        }
        return decodeJSON(r, obj)
    }
    
    func decodeJSON(r io.Reader, obj interface{}) error {
        decoder := json.NewDecoder(r)
    
        if err := decoder.Decode(obj); err != nil {
            return err
        }
        return validate(obj)
    }
    
    func validate(obj interface{}) error {
        if binding.Validator == nil {
            return nil
        }
        return binding.Validator.ValidateStruct(obj)
    }
    
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-02-05
      • 1970-01-01
      • 2018-09-30
      • 2016-12-22
      • 2011-06-02
      • 1970-01-01
      • 2019-06-15
      相关资源
      最近更新 更多