diff --git a/context.go b/context.go index 112f0ee0..e5a99b57 100644 --- a/context.go +++ b/context.go @@ -1080,6 +1080,33 @@ func (c *Context) GetRawData() ([]byte, error) { 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..d8ff2d66 100644 --- a/context_test.go +++ b/context_test.go @@ -2872,6 +2872,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") +}