feat: add warning for multiple response writes

When the response body is written multiple times in a single request,
Gin now prints a warning in debug mode. This helps developers identify
a common mistake where middleware or handlers accidentally write
responses more than once, resulting in invalid concatenated output.

The warning is printed via debugPrint() and only appears in debug mode,
making this change non-breaking and backwards compatible.

Fixes #4477

Example warning output:
[GIN-debug] [WARNING] Response body already written. Attempting to write again with status code 200

Changes:
- Added check in Context.Render() to detect multiple writes
- Added unit tests to verify warning behavior in debug mode
- Added test to verify no warning in release mode
This commit is contained in:
xingzihai 2026-03-30 03:48:15 +00:00
parent d3ffc99852
commit ec2c838995
2 changed files with 48 additions and 0 deletions

View File

@ -1158,6 +1158,10 @@ func (c *Context) Render(code int, r render.Render) {
return return
} }
if c.Writer.Written() {
debugPrint("[WARNING] Response body already written. Attempting to write again with status code %d", code)
}
if err := r.Render(c.Writer); err != nil { if err := r.Render(c.Writer); err != nil {
// Pushing error to c.Errors // Pushing error to c.Errors
_ = c.Error(err) _ = c.Error(err)

View File

@ -3808,3 +3808,47 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
}) })
} }
} }
func TestRenderMultipleWritesWarning(t *testing.T) {
// Test that a warning is printed when attempting to write response body multiple times
var w *httptest.ResponseRecorder
re := captureOutput(t, func() {
SetMode(DebugMode)
router := New()
router.GET("/test", func(c *Context) {
c.JSON(http.StatusOK, H{"first": "response"})
c.JSON(http.StatusOK, H{"second": "response"}) // Should trigger warning
})
w = httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/test", nil)
router.ServeHTTP(w, req)
SetMode(TestMode)
})
// Should contain the warning about multiple writes
assert.Contains(t, re, "[WARNING] Response body already written")
assert.Contains(t, re, "200")
// Verify the response body contains both responses (behavior unchanged)
assert.Contains(t, w.Body.String(), "first")
assert.Contains(t, w.Body.String(), "second")
}
func TestRenderMultipleWritesNoWarningInReleaseMode(t *testing.T) {
// Test that no warning is printed in release mode
re := captureOutput(t, func() {
SetMode(ReleaseMode)
router := New()
router.GET("/test", func(c *Context) {
c.JSON(http.StatusOK, H{"first": "response"})
c.JSON(http.StatusOK, H{"second": "response"})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/test", nil)
router.ServeHTTP(w, req)
SetMode(TestMode)
})
// Should not contain the warning in release mode
assert.NotContains(t, re, "[WARNING] Response body already written")
}