Golang gin接收JSON数据和图片。

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

Golang gin receive json data and image

问题

我有一个请求处理程序的代码:

func (h *Handlers) UpdateProfile() gin.HandlerFunc {
	type request struct {
		Username    string `json:"username" binding:"required,min=4,max=20"`
		Description string `json:"description" binding:"required,max=100"`
	}

	return func(c *gin.Context) {
		var updateRequest request

		if err := c.BindJSON(&updateRequest); err != nil {
			var validationErrors validator.ValidationErrors

			if errors.As(err, &validationErrors) {
				validateErrors := base.BindingError(validationErrors)
				c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": validateErrors})
			} else {
				c.AbortWithError(http.StatusBadRequest, err)
			}

			return
		}

		avatar, err := c.FormFile("avatar")
		if err != nil {
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
				"error": "image not contains in request",
			})
			return
		}

		log.Print(avatar)

		if avatar.Size > 3<<20 { // if avatar size more than 3mb
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
				"error": "image is too large",
			})
			return
		}

		file, err := avatar.Open()
		if err != nil {
			c.AbortWithError(http.StatusInternalServerError, err)
		}

		session := sessions.Default(c)
		id := session.Get("sessionId")
		log.Printf("ID type: %T", id)

		err = h.userService.UpdateProfile(fmt.Sprintf("%v", id), file, updateRequest.Username, updateRequest.Description)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{})
			return
		}

		c.IndentedJSON(http.StatusNoContent, gin.H{"message": "succesfull update"})
	}
}

我有一个针对此处理程序的单元测试:

func TestUser_UpdateProfile(t *testing.T) {
	type testCase struct {
		name               string
		image              io.Reader
		username           string
		description        string
		expectedStatusCode int
	}

	router := gin.Default()

	memStore := memstore.NewStore([]byte("secret"))
	router.Use(sessions.Sessions("session", memStore))

	userGroup := router.Group("user")
	repo := user.NewMemory()
	service := userService.New(repo)
	userHandlers.Register(userGroup, service)

	testImage := make([]byte, 100)
	rand.Read(testImage)
	image := bytes.NewReader(testImage)

	testCases := []testCase{
		{
			name:               "Request With Image",
			image:              image,
			username:           "bobik",
			description:        "wanna be sharik",
			expectedStatusCode: http.StatusNoContent,
		},
		{
			name:               "Request Without Image",
			image:              nil,
			username:           "sharik",
			description:        "wanna be bobik",
			expectedStatusCode: http.StatusNoContent,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			body := &bytes.Buffer{}
			writer := multipart.NewWriter(body)

			imageWriter, err := writer.CreateFormFile("avatar", "test_avatar.jpg")
			if err != nil {
				t.Fatal(err)
			}

			if _, err := io.Copy(imageWriter, image); err != nil {
				t.Fatal(err)
			}

			data := map[string]interface{}{
				"username":    tc.username,
				"description": tc.description,
			}
			jsonData, err := json.Marshal(data)
			if err != nil {
				t.Fatal(err)
			}

			jsonWriter, err := writer.CreateFormField("json")
			if err != nil {
				t.Fatal(err)
			}

			if _, err := jsonWriter.Write(jsonData); err != nil {
				t.Fatal(err)
			}

			writer.Close()

			// Creating request
			req := httptest.NewRequest(
				http.MethodPost,
				"http://localhost:8080/user/account/updateprofile",
				body,
			)
			req.Header.Set("Content-Type", writer.FormDataContentType())
			log.Print(req)

			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)

			assert.Equal(t, tc.expectedStatusCode, w.Result().StatusCode)
		})
	}
}

在测试过程中,我遇到了以下错误:
错误 #01: 数字文字中的无效字符'-'

这是请求体(我使用log.Print(req)打印出来的):

&{POST http://localhost:8080/user/account/updateprofile HTTP/1.1 1 1 map[Content-Type:[multipart/form-data; boundary=30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035]] {--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035
Content-Disposition: form-data; name="avatar"; filename="test_avatar.jpg"
Content-Type: application/octet-stream


--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035
Content-Disposition: form-data; name="json"

{"description":"wanna be bobik","username":"sharik"}
--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035--
} <nil> 414 [] false localhost:8080 map[] map[] <nil> map[] 192.0.2.1:1234 http://localhost:8080/user/account/updateprofile <nil> <nil> <nil> <nil>}

起初,我只是将json数据作为字符串,然后将其转换为字节。当出现错误时,我尝试使用json.Marshal将json数据转换为字节,但没有成功。我想用c.Bind解析json数据,并用c.FormFile解析给定的图像,这样可以吗?

更新:我将代码更改为先获取avatar,然后使用Bind结构获取json。现在我遇到了EOF错误。

英文:

I have this code for request handler:

func (h *Handlers) UpdateProfile() gin.HandlerFunc {
type request struct {
Username    string `json:&quot;username&quot; binding:&quot;required,min=4,max=20&quot;`
Description string `json:&quot;description&quot; binding:&quot;required,max=100&quot;`
}
return func(c *gin.Context) {
var updateRequest request
if err := c.BindJSON(&amp;updateRequest); err != nil {
var validationErrors validator.ValidationErrors
if errors.As(err, &amp;validationErrors) {
validateErrors := base.BindingError(validationErrors)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{&quot;error&quot;: validateErrors})
} else {
c.AbortWithError(http.StatusBadRequest, err)
}
return
}
avatar, err := c.FormFile(&quot;avatar&quot;)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
&quot;error&quot;: &quot;image not contains in request&quot;,
})
return
}
log.Print(avatar)
if avatar.Size &gt; 3&lt;&lt;20 { // if avatar size more than 3mb
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
&quot;error&quot;: &quot;image is too large&quot;,
})
return
}
file, err := avatar.Open()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
session := sessions.Default(c)
id := session.Get(&quot;sessionId&quot;)
log.Printf(&quot;ID type: %T&quot;, id)
err = h.userService.UpdateProfile(fmt.Sprintf(&quot;%v&quot;, id), file, updateRequest.Username, updateRequest.Description)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{})
return
}
c.IndentedJSON(http.StatusNoContent, gin.H{&quot;message&quot;: &quot;succesfull update&quot;})
}
}

And I have this unit test for this handler:

func TestUser_UpdateProfile(t *testing.T) {
type testCase struct {
name               string
image              io.Reader
username           string
description        string
expectedStatusCode int
}
router := gin.Default()
memStore := memstore.NewStore([]byte(&quot;secret&quot;))
router.Use(sessions.Sessions(&quot;session&quot;, memStore))
userGroup := router.Group(&quot;user&quot;)
repo := user.NewMemory()
service := userService.New(repo)
userHandlers.Register(userGroup, service)
testImage := make([]byte, 100)
rand.Read(testImage)
image := bytes.NewReader(testImage)
testCases := []testCase{
{
name:               &quot;Request With Image&quot;,
image:              image,
username:           &quot;bobik&quot;,
description:        &quot;wanna be sharik&quot;,
expectedStatusCode: http.StatusNoContent,
},
{
name:               &quot;Request Without Image&quot;,
image:              nil,
username:           &quot;sharik&quot;,
description:        &quot;wanna be bobik&quot;,
expectedStatusCode: http.StatusNoContent,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
body := &amp;bytes.Buffer{}
writer := multipart.NewWriter(body)
imageWriter, err := writer.CreateFormFile(&quot;avatar&quot;, &quot;test_avatar.jpg&quot;)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(imageWriter, image); err != nil {
t.Fatal(err)
}
data := map[string]interface{}{
&quot;username&quot;:    tc.username,
&quot;description&quot;: tc.description,
}
jsonData, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
jsonWriter, err := writer.CreateFormField(&quot;json&quot;)
if err != nil {
t.Fatal(err)
}
if _, err := jsonWriter.Write(jsonData); err != nil {
t.Fatal(err)
}
writer.Close()
// Creating request
req := httptest.NewRequest(
http.MethodPost,
&quot;http://localhost:8080/user/account/updateprofile&quot;,
body,
)
req.Header.Set(&quot;Content-Type&quot;, writer.FormDataContentType())
log.Print(req)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tc.expectedStatusCode, w.Result().StatusCode)
})
}
}

During test I have this error:
Error #01: invalid character '-' in numeric literal

And here is request body (I am printing it with log.Print(req)):

&amp;{POST http://localhost:8080/user/account/updateprofile HTTP/1.1 1 1 map[Content-Type:[multipart/form-data; boundary=30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035]] {--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035
Content-Disposition: form-data; name=&quot;avatar&quot;; filename=&quot;test_avatar.jpg&quot;
Content-Type: application/octet-stream
--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035
Content-Disposition: form-data; name=&quot;json&quot;
{&quot;description&quot;:&quot;wanna be bobik&quot;,&quot;username&quot;:&quot;sharik&quot;}
--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035--
} &lt;nil&gt; 414 [] false localhost:8080 map[] map[] &lt;nil&gt; map[] 192.0.2.1:1234 http://localhost:8080/user/account/updateprofile &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt;}

First I just have strings as json data and converted it to bytes. When error appeared I converted json data using json.Marshal, but it didn't work out. I want to parse json data with c.Bind and parse given image with c.FormFile, does it possible?

Upd. I replaced code to get avatar first, and then get json by Bind structure. Now I have EOF error.

答案1

得分: 3

TL;DR

我们可以定义一个结构体来同时接收JSON数据和图像文件(注意字段标签):

var updateRequest struct {
	Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
	User   struct {
		Username    string `json:"username" binding:"required,min=4,max=20"`
		Description string `json:"description" binding:"required,max=100"`
	} `form:"user" binding:"required"`
}

// c.ShouldBind会根据Content-Type头选择binding.FormMultipart。
// 我们调用c.ShouldBindWith来明确指定。
if err := c.ShouldBindWith(&updateRequest, binding.FormMultipart); err != nil {
	_ = c.AbortWithError(http.StatusBadRequest, err)
	return
}

Gin能自动解析multipart/form-data中的其他内容类型吗?

例如,xmlyaml

当前的Gin(@1.9.0)不会自动解析multipart/form-data中的xmlyamljson是幸运的,因为Gin碰巧在目标字段为结构体或映射时使用json.Unmarshal解析表单字段值。参见binding.setWithProperType

我们可以自己解析它们,就像这样(updateRequest.Event是来自表单的字符串值):

var event struct {
	At     time.Time `xml:"time" binding:"required"`
	Player string    `xml:"player" binding:"required"`
	Action string    `xml:"action" binding:"required"`
}

if err := binding.XML.BindBody([]byte(updateRequest.Event), &event); err != nil {
	_ = c.AbortWithError(http.StatusBadRequest, err)
	return
}

(请不要将application/xml请求中的xmlapplication/x-yaml请求中的yaml与此混淆。这仅在multipart/form-data请求中的xml内容或yaml内容时才需要)。

其他

  1. c.BindJSON不能用于从multipart/form-data中读取JSON,因为它假设请求体以有效的JSON开头。但实际上它以一个边界开始,看起来像--30b24345d...。这就是为什么它会失败并显示错误消息invalid character '-' in numeric literal
  2. 在调用c.FormFile("avatar")之后调用c.BindJSON不起作用,因为调用c.FormFile会读取整个请求体。而c.BindJSON后面没有内容可读取。这就是为什么你看到EOF错误的原因。

单个可运行文件中的演示

以下是完整的演示。使用go test ./... -v -count 1运行:

package m

import (
	"bytes"
	"crypto/rand"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/stretchr/testify/assert"
)

func handle(c *gin.Context) {
	var updateRequest struct {
		Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
		User   struct {
			Username    string `json:"username" binding:"required,min=4,max=20"`
			Description string `json:"description" binding:"required,max=100"`
		} `form:"user" binding:"required"`
		Event string `form:"event" binding:"required"`
	}

	// c.ShouldBind会根据Content-Type头选择binding.FormMultipart。
	// 我们调用c.ShouldBindWith来明确指定。
	if err := c.ShouldBindWith(&updateRequest, binding.FormMultipart); err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	fmt.Printf("%#v\n", updateRequest)

	var event struct {
		At     time.Time `xml:"time" binding:"required"`
		Player string    `xml:"player" binding:"required"`
		Action string    `xml:"action" binding:"required"`
	}

	if err := binding.XML.BindBody([]byte(updateRequest.Event), &event); err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	fmt.Printf("%#v\n", event)
}

func TestMultipartForm(t *testing.T) {
	testImage := make([]byte, 100)

	if _, err := rand.Read(testImage); err != nil {
		t.Fatal(err)
	}
	image := bytes.NewReader(testImage)

	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	imageWriter, err := writer.CreateFormFile("avatar", "test_avatar.jpg")
	if err != nil {
		t.Fatal(err)
	}

	if _, err := io.Copy(imageWriter, image); err != nil {
		t.Fatal(err)
	}

	if err := writer.WriteField("user", `{"username":"bobik","description":"wanna be sharik"}`); err != nil {
		t.Fatal(err)
	}

	xmlBody := `<?xml version="1.0" encoding="UTF-8"?>
<root>
   <time>2023-02-14T19:04:12Z</time>
   <player>playerOne</player>
   <action>strike (miss)</action>
</root>`
	if err := writer.WriteField("event", xmlBody); err != nil {
		t.Fatal(err)
	}

	writer.Close()

	req := httptest.NewRequest(
		http.MethodPost,
		"http://localhost:8080/update",
		body,
	)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	fmt.Printf("%v\n", req)

	w := httptest.NewRecorder()
	c, engine := gin.CreateTestContext(w)
	engine.POST("/update", handle)
	c.Request = req
	engine.HandleContext(c)

	assert.Equal(t, 200, w.Result().StatusCode)
}

谢谢阅读!

英文:

TL;DR

We can define a struct to receive the json data and image file at the same time (pay attention to the field tags):

var updateRequest struct {
	Avatar *multipart.FileHeader `form:&quot;avatar&quot; binding:&quot;required&quot;`
	User   struct {
		Username    string `json:&quot;username&quot; binding:&quot;required,min=4,max=20&quot;`
		Description string `json:&quot;description&quot; binding:&quot;required,max=100&quot;`
	} `form:&quot;user&quot; binding:&quot;required&quot;`
}

// c.ShouldBind will choose binding.FormMultipart based on the Content-Type header.
// We call c.ShouldBindWith to make it explicitly.
if err := c.ShouldBindWith(&amp;updateRequest, binding.FormMultipart); err != nil {
	_ = c.AbortWithError(http.StatusBadRequest, err)
	return
}

Can gin parse other content type in multipart/form-data automatically?

For example, xml or yaml.

The current gin (@1.9.0) does not parse xml or yaml in multipart/form-data automatically. json is lucky because gin happens to parse the form field value using json.Unmarshal when the target field is a struct or map. See binding.setWithProperType.

We can parse them ourself like this (updateRequest.Event is the string value from the form):

var event struct {
	At     time.Time `xml:&quot;time&quot; binding:&quot;required&quot;`
	Player string    `xml:&quot;player&quot; binding:&quot;required&quot;`
	Action string    `xml:&quot;action&quot; binding:&quot;required&quot;`
}

if err := binding.XML.BindBody([]byte(updateRequest.Event), &amp;event); err != nil {
	_ = c.AbortWithError(http.StatusBadRequest, err)
	return
}

(Please don't get confused with xml in an application/xml request or yaml in an application/x-yaml request. This is only required when the xml content or yaml content is in a multipart/form-data request).

Misc

  1. c.BindJSON can not be used to read json from multipart/form-data because it assumes that the request body starts with a valid json. But it's starts with a boundary, which looks like --30b24345d.... That's why it failed with error message invalid character &#39;-&#39; in numeric literal.
  2. Calling c.BindJSON after c.FormFile(&quot;avatar&quot;) does not work because calling c.FormFile makes the whole request body being read. And c.BindJSON has nothing to read later. That's why you see the EOF error.

The demo in a single runnable file

Here is the full demo. Run with go test ./... -v -count 1:

package m

import (
	&quot;bytes&quot;
	&quot;crypto/rand&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;mime/multipart&quot;
	&quot;net/http&quot;
	&quot;net/http/httptest&quot;
	&quot;testing&quot;
	&quot;time&quot;

	&quot;github.com/gin-gonic/gin&quot;
	&quot;github.com/gin-gonic/gin/binding&quot;
	&quot;github.com/stretchr/testify/assert&quot;
)

func handle(c *gin.Context) {
	var updateRequest struct {
		Avatar *multipart.FileHeader `form:&quot;avatar&quot; binding:&quot;required&quot;`
		User   struct {
			Username    string `json:&quot;username&quot; binding:&quot;required,min=4,max=20&quot;`
			Description string `json:&quot;description&quot; binding:&quot;required,max=100&quot;`
		} `form:&quot;user&quot; binding:&quot;required&quot;`
		Event string `form:&quot;event&quot; binding:&quot;required&quot;`
	}

	// c.ShouldBind will choose binding.FormMultipart based on the Content-Type header.
	// We call c.ShouldBindWith to make it explicitly.
	if err := c.ShouldBindWith(&amp;updateRequest, binding.FormMultipart); err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	fmt.Printf(&quot;%#v\n&quot;, updateRequest)

	var event struct {
		At     time.Time `xml:&quot;time&quot; binding:&quot;required&quot;`
		Player string    `xml:&quot;player&quot; binding:&quot;required&quot;`
		Action string    `xml:&quot;action&quot; binding:&quot;required&quot;`
	}

	if err := binding.XML.BindBody([]byte(updateRequest.Event), &amp;event); err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	fmt.Printf(&quot;%#v\n&quot;, event)
}

func TestMultipartForm(t *testing.T) {
	testImage := make([]byte, 100)

	if _, err := rand.Read(testImage); err != nil {
		t.Fatal(err)
	}
	image := bytes.NewReader(testImage)

	body := &amp;bytes.Buffer{}
	writer := multipart.NewWriter(body)

	imageWriter, err := writer.CreateFormFile(&quot;avatar&quot;, &quot;test_avatar.jpg&quot;)
	if err != nil {
		t.Fatal(err)
	}

	if _, err := io.Copy(imageWriter, image); err != nil {
		t.Fatal(err)
	}

	if err := writer.WriteField(&quot;user&quot;, `{&quot;username&quot;:&quot;bobik&quot;,&quot;description&quot;:&quot;wanna be sharik&quot;}`); err != nil {
		t.Fatal(err)
	}

	xmlBody := `&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;root&gt;
   &lt;time&gt;2023-02-14T19:04:12Z&lt;/time&gt;
   &lt;player&gt;playerOne&lt;/player&gt;
   &lt;action&gt;strike (miss)&lt;/action&gt;
&lt;/root&gt;`
	if err := writer.WriteField(&quot;event&quot;, xmlBody); err != nil {
		t.Fatal(err)
	}

	writer.Close()

	req := httptest.NewRequest(
		http.MethodPost,
		&quot;http://localhost:8080/update&quot;,
		body,
	)
	req.Header.Set(&quot;Content-Type&quot;, writer.FormDataContentType())
	fmt.Printf(&quot;%v\n&quot;, req)

	w := httptest.NewRecorder()
	c, engine := gin.CreateTestContext(w)
	engine.POST(&quot;/update&quot;, handle)
	c.Request = req
	engine.HandleContext(c)

	assert.Equal(t, 200, w.Result().StatusCode)
}

Thanks for reading!

答案2

得分: 1

我能够使用以下解决方案来满足您的需求。让我们从生产代码开始。

handlers.go 文件

package handlers

import (
	"io"
	"net/http"

	"github.com/gin-gonic/gin"
)

type Request struct {
	Username string `form:"username" binding:"required,min=4,max=20"`
	Avatar   []byte
}

func UpdateProfile(c *gin.Context) {
	avatarFileHeader, err := c.FormFile("avatar")
	if err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}

	file, err := avatarFileHeader.Open()
	if err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}
	data, err := io.ReadAll(file)
	if err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}
	var req Request
	req.Avatar = data
	if err := c.ShouldBind(&req); err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}
}

在这里,有几个需要注意的地方:

  1. 我为了演示的简化了解决方案。
  2. 我在 Username 字段上放置了注释 form:"username"。由于这个注释,gin 知道在传入的 HTTP 请求中查找该字段的位置。
  3. 然后,为了映射表单字段,我使用了内置的 ShouldBind 方法来处理其余部分。

现在,让我们切换到测试代码。

handlers_test.go 文件

测试文件只构建并运行了一个测试。不过,您肯定可以对其进行扩展。

package handlers

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/textproto"
	"os"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
)

func TestUpdateProfile(t *testing.T) {
	gin.SetMode(gin.TestMode)
	w := httptest.NewRecorder()
	c := gin.CreateTestContextOnly(w, gin.Default())

	// 创建 multipart writer
	body := new(bytes.Buffer)
	multipartWriter := multipart.NewWriter(body)

	// 添加文件
	fileHeader := make(textproto.MIMEHeader)
	fileHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "avatar", "avatar.png"))
	fileHeader.Set("Content-Type", "text/plain")
	writer, _ := multipartWriter.CreatePart(fileHeader)
	file, _ := os.Open("IvanPesenti.png")
	defer file.Close()
	io.Copy(writer, file)

	// 添加表单字段
	writer, _ = multipartWriter.CreateFormField("username")
	writer.Write([]byte("ivan_pesenti"))

	// 在发起 HTTP 请求之前,请确保关闭 writer
	multipartWriter.Close()
	c.Request = &http.Request{
		Header: make(http.Header),
	}
	c.Request.Method = http.MethodPost
	c.Request.Header.Set("Content-Type", multipartWriter.FormDataContentType())
	c.Request.Body = io.NopCloser(body)
	c.ContentType()

	UpdateProfile(c)

	assert.Equal(t, 200, w.Code)
}

在这里,我们可以总结以下内容:

  1. 我创建了 HTTP 请求、响应和一个 Gin 引擎来处理测试。
  2. 我创建了一个 multipart/form-data 请求,用于传递给 UpdateProfile Gin 处理程序。
  3. 我读取了本地图像并设置了表单文件 avatar
  4. 我使用值 ivan_pesenti 设置了表单字段 username
  5. 在发出请求之前,我关闭了 multipartWriter。这是必要的!

测试文件的其余部分应该很简单,所以我不会花额外的时间来解释它!

如果您对此清楚或有其他问题,请告诉我,谢谢!

英文:

I was able to manage your needs with the following solution. Let's start with the production code.

handlers.go file

package handlers

import (
	&quot;io&quot;
	&quot;net/http&quot;

	&quot;github.com/gin-gonic/gin&quot;
)

type Request struct {
	Username string `form:&quot;username&quot; binding:&quot;required,min=4,max=20&quot;`
	Avatar []byte
}

func UpdateProfile(c *gin.Context) {
	avatarFileHeader, err := c.FormFile(&quot;avatar&quot;)
	if err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}

	file, err := avatarFileHeader.Open()
	if err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}
	data, err := io.ReadAll(file)
	if err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}
	var req Request
	req.Avatar = data
	if err := c.ShouldBind(&amp;req); err != nil {
		c.String(http.StatusBadRequest, err.Error())
		return
	}
}

Here, there are a couple of things to be aware of:

  1. I simplified the solution just for the sake of the demo.
  2. I put the annotation form:&quot;username&quot; on the Username field. Thanks to this, gin knows where to look for the field in the incoming HTTP Request.
  3. Then, to map the form fields, I used the built-in method ShouldBind that takes care of the rest.

Now, let's switch to the test code.

handlers_test.go file

The test file builds and runs only a single test. However, you can surely expand on it.

package handlers

import (
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;mime/multipart&quot;
	&quot;net/http&quot;
	&quot;net/http/httptest&quot;
	&quot;net/textproto&quot;
	&quot;os&quot;
	&quot;testing&quot;

	&quot;github.com/gin-gonic/gin&quot;
	&quot;github.com/stretchr/testify/assert&quot;
)

func TestUpdateProfile(t *testing.T) {
	gin.SetMode(gin.TestMode)
	w := httptest.NewRecorder()
	c := gin.CreateTestContextOnly(w, gin.Default())

	// multipart writer creation
	body := new(bytes.Buffer)
	multipartWriter := multipart.NewWriter(body)

	// add file
	fileHeader := make(textproto.MIMEHeader)
	fileHeader.Set(&quot;Content-Disposition&quot;, fmt.Sprintf(`form-data; name=&quot;%s&quot;; filename=&quot;%s&quot;`, &quot;avatar&quot;, &quot;avatar.png&quot;))
	fileHeader.Set(&quot;Content-Type&quot;, &quot;text/plain&quot;)
	writer, _ := multipartWriter.CreatePart(fileHeader)
	file, _ := os.Open(&quot;IvanPesenti.png&quot;)
	defer file.Close()
	io.Copy(writer, file)

	// add form field
	writer, _ = multipartWriter.CreateFormField(&quot;username&quot;)
	writer.Write([]byte(&quot;ivan_pesenti&quot;))

	// please be sure to close the writer before launching the HTTP Request
	multipartWriter.Close()
	c.Request = &amp;http.Request{
		Header: make(http.Header),
	}
	c.Request.Method = http.MethodPost
	c.Request.Header.Set(&quot;Content-Type&quot;, multipartWriter.FormDataContentType())
	c.Request.Body = io.NopCloser(body)
	c.ContentType()

	UpdateProfile(c)

	assert.Equal(t, 200, w.Code)
}

Here, we can summarize what's going on in the following list:

  1. I created the HTTP Request, Response, and a Gin engine to handle the test.
  2. I created a multipart/form-data request to pass to the UpdateProfile Gin handler.
  3. I read a local image and set the form file avatar.
  4. I set the form field username with the value ivan_pesenti.
  5. I closed the multipartWriter before issuing the request. This is imperative!

The rest of the test file should be pretty straight-forward, so I won't spend extra time explaining it!

Let me know whether it's clear or you have other questions, thanks!

答案3

得分: 1

> 还可以帮我在这里实现会话设置吗?因为我遇到了这个错误:
>
> &gt; panic: Key "github.com/gin-contrib/sessions" does not exist &gt;
>
> 当我添加了这段代码后:
>
> go &gt; // Create a Gin context from the test request and recorder &gt; c, _ := gin.CreateTestContext(w) &gt; c.Request = req &gt; session := sessions.Default(c) &gt; session.Set("sessionId", uuid.New()) &gt; session.Save() &gt;

这是一个与原始问题无关的新问题。所以我将为它发布一个新的答案(也许我们应该创建一个新的问题。如果创建了一个新的问题,我将把这个答案移到新的问题中)。

错误是由于会话尚未添加到上下文中引起的。我将尝试用一个序列图来解释会话的工作原理。

Golang gin接收JSON数据和图片。

你可以看到,在会话中间件执行请求之前,sessions.Default(c) 是不可用的(参见步骤2和步骤7)。

因此,自然而然地在会话中间件之后添加一个中间件,以便它可以访问和修改会话:

package m

import (
	"io"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/memstore"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

// 一个读取会话数据的处理程序。
func handle(c *gin.Context) {
	session := sessions.Default(c)

	c.String(http.StatusOK, session.Get("sessionId").(string))
}

func TestSession(t *testing.T) {
	w := httptest.NewRecorder()
	c, engine := gin.CreateTestContext(w)

	memStore := memstore.NewStore([]byte("secret"))
	engine.Use(sessions.Sessions("session", memStore))

	// 在会话中间件之后添加一个中间件,以便它可以访问和修改会话。
	sessionId := uuid.NewString()
	engine.Use(gin.HandlerFunc(func(c *gin.Context) {
		session := sessions.Default(c)
		session.Set("sessionId", sessionId)
		c.Next()
	}))

	engine.GET("/session", handle)

	c.Request = httptest.NewRequest(http.MethodGet, "http://localhost/session", nil)
	engine.HandleContext(c)

	if buf, err := io.ReadAll(w.Body); err != nil {
		t.Fatal(err)
	} else if string(buf) != sessionId {
		t.Errorf("got sessionId %q, want %q", buf, sessionId)
	}
}

注意:由于测试中触碰了会话,如果会话出现问题,可能测试无法捕捉到。因此,不要忘记添加一个测试,发送一个请求来创建真实的会话,并将这个请求的 cookies(假设它使用 cookies)传递给下一个读取会话的请求。

英文:

> And also may you help me implement session set here pls? Golang gin接收JSON数据和图片。 Because I got this error:
>
>
&gt; panic: Key &quot;github.com/gin-contrib/sessions&quot; does not exist
&gt;

>
> When I added this code:
>
> go
&gt; // Create a Gin context from the test request and recorder
&gt; c, _ := gin.CreateTestContext(w)
&gt; c.Request = req
&gt; session := sessions.Default(c)
&gt; session.Set(&quot;sessionId&quot;, uuid.New())
&gt; session.Save()
&gt;

This is a new question that has nothing to do with the original question. So I will post a new answer for it (maybe we should create a new question instead. I will move this answer to the new question if one is created).

The error is caused by the fact that the session is not added to the context yet. I will try to explain how session works generally with q sequence diagram.

Golang gin接收JSON数据和图片。

You see that before the session middleware is executed for a request, sessions.Default(c) is not available yet (see step 2 and step 7).

So it's naturally to add a middleware after the session middleware so that it can access and modify the session:

package m

import (
	&quot;io&quot;
	&quot;net/http&quot;
	&quot;net/http/httptest&quot;
	&quot;testing&quot;

	&quot;github.com/gin-contrib/sessions&quot;
	&quot;github.com/gin-contrib/sessions/memstore&quot;
	&quot;github.com/gin-gonic/gin&quot;
	&quot;github.com/google/uuid&quot;
)

// A handler that reads session data.
func handle(c *gin.Context) {
	session := sessions.Default(c)

	c.String(http.StatusOK, session.Get(&quot;sessionId&quot;).(string))
}

func TestSession(t *testing.T) {
	w := httptest.NewRecorder()
	c, engine := gin.CreateTestContext(w)

	memStore := memstore.NewStore([]byte(&quot;secret&quot;))
	engine.Use(sessions.Sessions(&quot;session&quot;, memStore))

	// Add a middleware after the session middleware so that it can
	// access and modify the session.
	sessionId := uuid.NewString()
	engine.Use(gin.HandlerFunc(func(c *gin.Context) {
		session := sessions.Default(c)
		session.Set(&quot;sessionId&quot;, sessionId)
		c.Next()
	}))

	engine.GET(&quot;/session&quot;, handle)

	c.Request = httptest.NewRequest(http.MethodGet, &quot;http://localhost/session&quot;, nil)
	engine.HandleContext(c)

	if buf, err := io.ReadAll(w.Body); err != nil {
		t.Fatal(err)
	} else if string(buf) != sessionId {
		t.Errorf(&quot;got sessionId %q, want %q&quot;, buf, sessionId)
	}
}

Notes: Since the session is touched in the test, if there is something wrong with the session, maybe the test can not catch it. So don't forget to add a test to make an request to let it create the real session, and pass the cookies from this request (let's assume it uses cookies) to the next request that will read from the session.

huangapple
  • 本文由 发表于 2023年4月11日 22:33:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/75987082.html
匿名

发表评论

匿名网友

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

确定