I have this code for request handler:
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"})
}
}
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("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)
})
}
}
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)):
&{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>}
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.
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:"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 will choose binding.FormMultipart based on the Content-Type header.
// We call c.ShouldBindWith to make it explicitly.
if err := c.ShouldBindWith(&updateRequest, binding.FormMultipart); err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
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:"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
}
(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).
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 '-' in numeric literal
.c.BindJSON
after c.FormFile("avatar")
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.Here is the full demo. Run with 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 will choose binding.FormMultipart based on the Content-Type header.
// We call c.ShouldBindWith to make it explicitly.
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)
}
Thanks for reading!