0

I'm getting a weird error which previously didn't appear for my code, which might be caused by my JWT implementation that I didn't test thoroughly tbh. So I have this current code:

ginGine.POST("/edge_device_info", func(ctx *gin.Context) {
    reqMap := helpers.GetJSONFromReqBody(ctx.Request.Body, ctx)
    accessToken := fmt.Sprintf("%v", reqMap["access_token"])

    tokenValid, jwtErr := srvHelpers.ValidateJWTToken(accessToken)

    if jwtErr != nil || !tokenValid {
        fmt.Println("invalid token")
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error0": jwtErr.Error(),
        })
        return
    }
    
    // reqBody, err := json.Marshal(reqMap)
    // if err != nil {
    //  ctx.JSON(http.StatusBadRequest, gin.H{
    //      "error10": err.Error(),
    //  })
    //  return
    // }

    var edge_device_info models.EdgeDeviceInfo

    // err = json.Unmarshal(reqBody, &edge_device_info)
    err := ctx.ShouldBind(&edge_device_info)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error1": err.Error(),
        })
        return
    }

    // more code
})

This API endpoint gets sent this similar looking json:

{
    "name": "Some text",
    "description": "Another bunch of text",
    "access_token": "SOME_JWT_TOKEN"
}

Now, this json looks fine, right? But ShouldBind() doesn't like that and would return an EOF. If you check the top part of my code, I'm converting the received JSON body to a map then use the access_token, which is a JWT token, to validate the user's access. I'm not quite sure if that process somehow made that issue, and that's what I wanted to know. That's the reason I made this question. I already found the workaround, which is the commented code above. Which is quite weird as creating the map in the first place means that the request body's JSON was valid.

rminaj
  • 560
  • 6
  • 28
  • You're correct. The request body has been read, so the second read gets `EOF`. It's better to move the `access_token` out of the request body. For example, put it in the request header. – Zeke Lu Jun 13 '23 at 08:26
  • Hmm, so reading the request body removes it's contents? I kinda have an assumption that might be the case. Will consider that suggestion – rminaj Jun 13 '23 at 09:02
  • It's better to treat the request body as a stream. Every time `Read` is called, it gives you contents from the current position and move the position forward. And it does not keep the contents that already been read. So you can not read again from the reader. As a contrast, a file reader implements the `io.Seeker` interface, and you can seek the position to the begin of the file and read again. – Zeke Lu Jun 13 '23 at 09:20
  • 1
    For the sake of completeness, the [(*Context).ShouldBindBodyWith](https://pkg.go.dev/github.com/gin-gonic/gin#Context.ShouldBindBodyWith) method will read the request body and stores it into the context, so you can access the request body again. But it's preferred to move the `access_token` out of the request body. – Zeke Lu Jun 13 '23 at 09:26

1 Answers1

1

As correctly pointed out by @Zeke Lu, you should use the method ShouldBindBodyWith on the gin.Context struct. I've written a small example for you to use.

package main

import (
    "fmt"
    "net/http"

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

type Todo struct {
    Description string `json:"description" binding:"required"`
}

func Todos(c *gin.Context) {
    // read the body the first time
    var todo Todo
    if err := c.ShouldBindBodyWith(&todo, binding.JSON); err != nil {
        c.JSON(http.StatusBadRequest, err)
        return
    }
    fmt.Println("first time:", todo)

    // read the body again
    var todoSecondTime Todo
    if err := c.ShouldBindBodyWith(&todoSecondTime, binding.JSON); err != nil {
        c.JSON(http.StatusBadRequest, err)
        return
    }
    fmt.Println("second time:", todoSecondTime)
}

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

    r.POST("/todos", Todos)

    r.Run(":8080")
}

The code is pretty simple but it's purpose is to just provide a valuable example. If you're going to read the body more than once, like in your scenario, you should rely on this method as it does a copy of the request payload into the context. Thanks to this, the subsequent calls will use that body stream for binding it again.

Please note that, if you're going to read the body only once, you should rely on the ShouldBindWith method due to performance reasons.

Let me know if this example clarifies your doubts, thanks!

ossan
  • 1,665
  • 4
  • 10