显式处理绑定之前的gzip压缩的JSON。

huangapple go评论147阅读模式
英文:

Explicitly handle gzipped json before binding

问题

我想编写一个API,它将使用POST方法发送gzip压缩的JSON数据。虽然下面的代码可以处理请求体中的简单JSON数据,但无法处理gzip压缩的JSON数据。

在使用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

期望结果

<!-- 你对于 'curl' 命令的期望结果,例如 -->

$ 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版本:go version go1.17.2 darwin/amd64
  • Gin版本(或提交引用):v1.7.4
  • 操作系统:MacOS Monterey
英文:

I want to write an api that will be sent gzipped json data with POST. While the below can deal with simple json in the body, it does not deal if the json is gzipped.

Do we need to explicitly handle the decompression before using c.ShouldBindJSON?

How to reproduce

package main

import (
	&quot;github.com/gin-gonic/gin&quot;
	&quot;log&quot;
	&quot;net/http&quot;
)

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

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

Expectations

<!-- Your expectation result of 'curl' command, like -->

$ echo &#39;{&quot;data&quot; : &quot;hello&quot;}&#39; | gzip | curl -v -i -X POST -H &quot;Content-Type: application/json&quot; -H &quot;Content-Encoding: gzip&quot; --data-binary @- localhost:8080/postgzip
hello

Actual result

<!-- Actual result showing the problem -->

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

Environment

  • go version: go version go1.17.2 darwin/amd64
  • gin version (or commit ref): v1.7.4
  • operating system: MacOS Monterey

答案1

得分: 1

我们在使用c.ShouldBindJSON之前需要明确处理解压缩吗?

当然需要。Gin的ShouldBindJSON对于负载的编码方式一无所知。它期望的输入是JSON,正如方法名所示。

如果你希望编写可重用的代码,你可以实现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{})
英文:

> Do we need to explicitly handle the decompression before using c.ShouldBindJSON ?

Of course. Gin ShouldBindJSON knows nothing of how your payload may or may not be encoded. It expects JSON input, as the method name suggests.

If you wish to write reusable code, you can implement the Binding interface.

A very minimal example:

type GzipJSONBinding struct {
}

func (b *GzipJSONBinding) Name() string {
	return &quot;gzipjson&quot;
}

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

And then usable with c.ShouldBindWith, which allows using an arbitrary binding engine:

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

答案2

得分: 0

完全可用的示例,开启 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)
}

英文:

Fully working example that switches on Content-Encoding

curl to try to use it

$ echo &#39;{&quot;data&quot; : &quot;hello&quot;}&#39; | gzip | curl -X POST -H &quot;Content-Type: application/json&quot; -H &quot;Content-Encoding: gzip&quot; --data-binary @- localhost:8080/json
hello
$ curl -X POST -H &quot;Content-Type: application/json&quot; --data-raw &#39;{&quot;data&quot; : &quot;hello&quot;}&#39; localhost:8080/json
hello
package main

import (
	&quot;bytes&quot;
	&quot;compress/gzip&quot;
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;github.com/gin-gonic/gin&quot;
	&quot;github.com/gin-gonic/gin/binding&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;net/http&quot;
)

type PostData struct {
	Data string `binding:&quot;required&quot; json:&quot;data&quot;`
}

func main() {
	r := gin.Default()
	r.POST(&quot;/json&quot;, func(c *gin.Context) {

		var postdata PostData

		contentEncodingHeader := c.GetHeader(&quot;Content-Encoding&quot;)
		switch contentEncodingHeader {
		case &quot;gzip&quot;:
			if err := c.ShouldBindBodyWith(&amp;postdata, gzipJSONBinding{}); err != nil {
				log.Println(&quot;Error parsing GZIP JSON request body&quot;, &quot;error&quot;, err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
				return
			}
		case &quot;&quot;:
			if err := c.ShouldBindJSON(&amp;postdata); err != nil {
				log.Println(&quot;Error parsing JSON request body&quot;, &quot;error&quot;, err)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{&quot;error&quot;: err.Error()})
				return
			}
		default:
			log.Println(&quot;unsupported Content-Encoding&quot;)
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{&quot;error&quot;: &quot;unsupported Content-Encoding&quot;})
			return

		}

		log.Printf(&quot;%s&quot;, postdata)
		if !c.IsAborted() {
			c.String(200, postdata.Data)
		}
	})
	r.Run()
}

type gzipJSONBinding struct{}

func (gzipJSONBinding) Name() string {
	return &quot;gzipjson&quot;
}

func (gzipJSONBinding) Bind(req *http.Request, obj interface{}) error {
	if req == nil || req.Body == nil {
		return fmt.Errorf(&quot;invalid request&quot;)
	}
	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)
}

huangapple
  • 本文由 发表于 2021年11月7日 05:41:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/69868116.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定