diff --git a/context.go b/context.go index 112f0ee0..a9ea1ba3 100644 --- a/context.go +++ b/context.go @@ -5,6 +5,7 @@ package gin import ( + "bytes" "errors" "fmt" "io" @@ -914,6 +915,24 @@ func (c *Context) ShouldBindUri(obj any) error { // ShouldBindWith binds the passed struct pointer using the specified binding engine. // See the binding package. func (c *Context) ShouldBindWith(obj any, b binding.Binding) error { + if b.Name() == "form-urlencoded" || b.Name() == "form" { + var body []byte + if cb, ok := c.Get(BodyBytesKey); ok { + if cbb, ok := cb.([]byte); ok { + body = cbb + } + } + + if body == nil { + var err error + body, err = io.ReadAll(c.Request.Body) + if err != nil { + return err + } + c.Set(BodyBytesKey, body) + } + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + } return b.Bind(c.Request, obj) } @@ -1077,9 +1096,43 @@ func (c *Context) GetRawData() ([]byte, error) { if c.Request.Body == nil { return nil, errors.New("cannot read nil body") } + + if cb, ok := c.Get(BodyBytesKey); ok { + if cbb, ok := cb.([]byte); ok { + return cbb, nil + } + } + return io.ReadAll(c.Request.Body) } +// GetRequestBody returns the request body as bytes, caching it for reuse. +// This allows the body to be read multiple times without being consumed. +// Returns an error if the body cannot be read or if it's nil. +func (c *Context) GetRequestBody() ([]byte, error) { + if c.Request.Body == nil { + return nil, errors.New("cannot read nil body") + } + + // Check if body is already cached + if cb, ok := c.Get(BodyBytesKey); ok { + if cbb, ok := cb.([]byte); ok { + return cbb, nil + } + } + + // Read and cache the body + body, err := io.ReadAll(c.Request.Body) + if err != nil { + return nil, err + } + + // Cache the body for future use + c.Set(BodyBytesKey, body) + + return body, nil +} + // SetSameSite with cookie func (c *Context) SetSameSite(samesite http.SameSite) { c.sameSite = samesite diff --git a/context_test.go b/context_test.go index 126646fc..2bc4314c 100644 --- a/context_test.go +++ b/context_test.go @@ -2349,6 +2349,28 @@ func TestContextShouldBindWithQuery(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextGetRawDataAfterShouldBind(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("p1=1&p2=2&p3=4")) + c.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + var s struct { + P1 string `form:"p1"` + P2 string `form:"p2"` + P3 string `form:"p3"` + } + require.NoError(t, c.ShouldBind(&s)) + assert.Equal(t, "1", s.P1) + assert.Equal(t, "2", s.P2) + assert.Equal(t, "4", s.P3) + + rawData, err := c.GetRawData() + require.NoError(t, err) + assert.Equal(t, "p1=1&p2=2&p3=4", string(rawData)) +} + func TestContextShouldBindWithYAML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -2872,6 +2894,105 @@ func TestContextGetRawData(t *testing.T) { assert.Equal(t, "Fetch binary post data", string(data)) } +func TestContextGetRequestBody(t *testing.T) { + // Test basic functionality + t.Run("basic functionality", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + bodyContent := "test request body data" + body := strings.NewReader(bodyContent) + c.Request, _ = http.NewRequest(http.MethodPost, "/", body) + + data, err := c.GetRequestBody() + require.NoError(t, err) + assert.Equal(t, bodyContent, string(data)) + }) + + // Test multiple calls return cached data + t.Run("multiple calls return cached data", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + bodyContent := "reusable request body" + body := strings.NewReader(bodyContent) + c.Request, _ = http.NewRequest(http.MethodPost, "/", body) + + // First call + data1, err1 := c.GetRequestBody() + require.NoError(t, err1) + assert.Equal(t, bodyContent, string(data1)) + + // Second call should return cached data + data2, err2 := c.GetRequestBody() + require.NoError(t, err2) + assert.Equal(t, bodyContent, string(data2)) + + // Third call should also return cached data + data3, err3 := c.GetRequestBody() + require.NoError(t, err3) + assert.Equal(t, bodyContent, string(data3)) + + // All calls should return the same data + assert.Equal(t, data1, data2) + assert.Equal(t, data2, data3) + }) + + // Test nil body error + t.Run("nil body error", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest(http.MethodPost, "/", nil) + c.Request.Body = nil + + data, err := c.GetRequestBody() + assert.Error(t, err) + assert.Nil(t, data) + assert.Contains(t, err.Error(), "cannot read nil body") + }) + + // Test empty body + t.Run("empty body", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + body := strings.NewReader("") + c.Request, _ = http.NewRequest(http.MethodPost, "/", body) + + data, err := c.GetRequestBody() + require.NoError(t, err) + assert.Equal(t, "", string(data)) + assert.Equal(t, 0, len(data)) + }) + + // Test large body + t.Run("large body", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + largeContent := strings.Repeat("large body content ", 1000) + body := strings.NewReader(largeContent) + c.Request, _ = http.NewRequest(http.MethodPost, "/", body) + + data, err := c.GetRequestBody() + require.NoError(t, err) + assert.Equal(t, largeContent, string(data)) + assert.Equal(t, len(largeContent), len(data)) + }) + + // Test integration with ShouldBindBodyWith + t.Run("integration with ShouldBindBodyWith", func(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + jsonBody := `{"name":"test","value":123}` + body := strings.NewReader(jsonBody) + c.Request, _ = http.NewRequest(http.MethodPost, "/", body) + c.Request.Header.Set("Content-Type", "application/json") + + // Get body first + bodyData, err := c.GetRequestBody() + require.NoError(t, err) + assert.Equal(t, jsonBody, string(bodyData)) + + // ShouldBindBodyWith should still work and reuse cached body + var jsonData map[string]interface{} + err = c.ShouldBindBodyWithJSON(&jsonData) + require.NoError(t, err) + assert.Equal(t, "test", jsonData["name"]) + assert.Equal(t, float64(123), jsonData["value"]) + }) +} + func TestContextRenderDataFromReader(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) diff --git a/examples/get_request_body_example.go b/examples/get_request_body_example.go new file mode 100644 index 00000000..33dd07c7 --- /dev/null +++ b/examples/get_request_body_example.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + // Example middleware that reads the request body + r.Use(func(c *gin.Context) { + // Get the request body - this can be called multiple times + body, err := c.GetRequestBody() + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + // Log the body length for demonstration + fmt.Printf("Middleware: Request body length: %d bytes\n", len(body)) + + // Store body in context for use by handlers + c.Set("rawBody", body) + c.Next() + }) + + // Handler that also reads the body + r.POST("/echo", func(c *gin.Context) { + // Get the body again - this will use the cached version + body, err := c.GetRequestBody() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Also get the body that was stored by middleware + storedBody, exists := c.Get("rawBody") + if !exists { + c.JSON(http.StatusInternalServerError, gin.H{"error": "body not found in context"}) + return + } + + // Both should be identical + if string(body) != string(storedBody.([]byte)) { + c.JSON(http.StatusInternalServerError, gin.H{"error": "bodies don't match"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Body received and cached successfully", + "body": string(body), + "length": len(body), + }) + }) + + // Handler that uses binding after GetRequestBody + r.POST("/bind-after-body", func(c *gin.Context) { + // First bind to a struct - this caches the body + var jsonData map[string]interface{} + if err := c.ShouldBindBodyWithJSON(&jsonData); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Then get the raw body - this will use the cached version + rawBody, err := c.GetRequestBody() + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "raw_body": string(rawBody), + "parsed_data": jsonData, + }) + }) + + fmt.Println("Server starting on :9090") + fmt.Println("Try: curl -X POST -d 'hello world' http://localhost:9090/echo") + fmt.Println("Or: curl -X POST -H 'Content-Type: application/json' -d '{\"name\":\"test\"}' http://localhost:9090/bind-after-body") + + r.Run(":9090") +} diff --git a/gin_integration_test.go b/gin_integration_test.go index 3ea5fe2f..81e18c12 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -16,6 +16,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "sync" "testing" @@ -64,7 +65,16 @@ func testRequest(t *testing.T, params ...string) { } func TestRunEmpty(t *testing.T) { - os.Setenv("PORT", "") + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + require.NoError(t, err) + l, err := net.ListenTCP("tcp", addr) + require.NoError(t, err) + port := strconv.Itoa(l.Addr().(*net.TCPAddr).Port) + l.Close() + + os.Setenv("PORT", port) + defer os.Unsetenv("PORT") + router := New() go func() { router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) @@ -75,8 +85,8 @@ func TestRunEmpty(t *testing.T) { err := waitForServerReady("http://localhost:8080/example", 10) require.NoError(t, err, "server should start successfully") - require.Error(t, router.Run(":8080")) - testRequest(t, "http://localhost:8080/example") + require.Error(t, router.Run(":"+port)) + testRequest(t, "http://localhost:"+port+"/example") } func TestBadTrustedCIDRs(t *testing.T) {