How to Resolve "Gin Binding Error: EOF When Binding JSON Body Twice"
Quick answer
Your Gin handler calls a binding function like c.ShouldBindJSON() a second time β maybe in a middleware and then again in the handler β and the second call...
Your Gin handler calls a binding function like c.ShouldBindJSON() a second time β maybe in a middleware and then again in the handler β and the second call fails with EOF instead of parsing the body. This happens because an HTTP request body is a stream that can only be read once.
The Problem
The first bind works fine, but a second attempt anywhere later in the request lifecycle fails:
$ curl -X POST http://localhost:8080/users -d '{"name":"Alice"}'
{"error":"EOF"}
In code, it typically looks like this β a logging middleware or validation layer reads the body first, then the actual handler tries to bind it again:
func LogRequestBody() gin.HandlerFunc {
return func(c *gin.Context) {
var payload map[string]interface{}
c.ShouldBindJSON(&payload) // reads and drains the body here
log.Printf("request body: %v", payload)
c.Next()
}
}
func CreateUser(c *gin.Context) {
var input UserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": err.Error()}) // returns {"error":"EOF"}
return
}
}
Why It Happens
The HTTP request body in Go's net/http is an io.ReadCloser β a one-way stream, not a buffer you can rewind. The first call to ShouldBindJSON (or any function reading c.Request.Body) consumes the entire stream. By the time a second call tries to read it, there's nothing left, so the JSON decoder immediately hits end-of-file and returns EOF. This shows up in a few common patterns:
- A logging or auditing middleware reads and logs the request body, then the actual handler tries to bind it again later in the chain.
- A validation middleware binds the body to check something, then calls
c.Next(), and the handler binds it a second time. - The same handler accidentally calls a binding function twice, for example once for validation and again for actual processing.
- A retry or error-handling path re-attempts binding without realizing the body was already consumed on the first attempt.
The Fix
The cleanest fix is to bind the body exactly once and pass the parsed struct forward instead of re-reading raw bytes. Store the result in the Gin context for downstream handlers to reuse:
func BindOnce() gin.HandlerFunc {
return func(c *gin.Context) {
var input UserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
c.Abort()
return
}
c.Set("input", input)
c.Next()
}
}
func CreateUser(c *gin.Context) {
input := c.MustGet("input").(UserInput)
// use input directly, no second bind needed
}
If you genuinely need to read the raw body more than once β for logging plus binding, for example β buffer it manually and restore it after each read:
func BufferBody() gin.HandlerFunc {
return func(c *gin.Context) {
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
log.Printf("request body: %s", bodyBytes)
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // restore for next reader
c.Next()
}
}
With this middleware in place before the handler, c.ShouldBindJSON() in the actual handler now sees a fresh, unread body and binds successfully.
Still Not Working?
If you're still seeing EOF after buffering the body once, double-check that every middleware in the chain that touches c.Request.Body restores it the same way β a single unrestored read anywhere in the chain breaks it for everything downstream. You can confirm the body is actually intact right before your handler runs by logging its length immediately before the final bind call:
bodyBytes, _ := io.ReadAll(c.Request.Body)
log.Printf("body length before final bind: %d", len(bodyBytes))
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
A length of zero confirms something upstream already drained it without restoring, and you'll need to trace back through your middleware chain to find that unrestored read.
It's also worth knowing about Gin's built-in alternative for cases where you specifically need to inspect the raw body without permanently consuming it: c.GetRawData() reads the body once but doesn't automatically restore it either, so it has the exact same pitfall as manually calling io.ReadAll β you still need to reassign c.Request.Body afterward if anything downstream needs to read it again:
rawData, err := c.GetRawData()
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData)) // still required
For high-traffic services, be mindful that buffering the entire body into memory on every request β as shown in the middleware examples above β has a real cost at scale. If your API accepts large payloads, consider capping the buffered size explicitly, so a malicious or buggy client sending an enormous body can't exhaust server memory through this exact logging or validation path:
limitedReader := io.LimitReader(c.Request.Body, 1<<20) // cap at 1 MB
bodyBytes, err := io.ReadAll(limitedReader)