-
Notifications
You must be signed in to change notification settings - Fork 8.6k
Description
Feature Description
Add Warning for Multiple Response Writes
Description
Currently, Gin silently allows multiple response writing methods (like c.JSON(), c.XML(), c.String(), etc.) to be called in the same request, which concatenates their output into a single response body. This is almost never the intended behavior and can lead to subtle bugs that are difficult to debug.
While Gin does warn when trying to change the status code after headers are written, there is no warning when writing the response body multiple times.
Current Behavior
Example Code
router.GET("/example", func(c *gin.Context) {
c.JSON(200, gin.H{"first": "response"})
c.JSON(200, gin.H{"second": "response"}) // No warning!
})Actual Output
{"first":"response"}{"second":"response"}
The response body contains both JSON objects concatenated together, which is invalid JSON and not what developers expect.
Existing Test Case
The test TestMiddlewareWrite in middleware_test.go:229-250 explicitly tests this behavior:
func TestMiddlewareWrite(t *testing.T) {
router := New()
router.Use(func(c *Context) {
c.String(http.StatusBadRequest, "hola\n")
})
router.Use(func(c *Context) {
c.XML(http.StatusBadRequest, H{"foo": "bar"})
})
router.Use(func(c *Context) {
c.JSON(http.StatusBadRequest, H{"foo": "bar"})
})
router.GET("/", func(c *Context) {
c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}, func(c *Context) {
c.Render(http.StatusBadRequest, sse.Event{
Event: "test",
Data: "message",
})
})
w := PerformRequest(router, http.MethodGet, "/")
// All outputs are concatenated:
// "hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n"
assert.Equal(t, http.StatusBadRequest, w.Code)
}Problem
- Silent Failures: Developers don't realize they're writing the response multiple times until they inspect the actual HTTP response
- Invalid Output: Multiple JSON/XML responses concatenated together produce invalid output
- Difficult Debugging: This issue often occurs across middleware and handlers, making it hard to track down
- Inconsistent: Gin already warns about duplicate header writes but not body writes
Comparison with Existing Warning
Gin currently warns when trying to change status codes after headers are written:
File: response_writer.go:70
func (w *responseWriter) WriteHeader(code int) {
if code > 0 && w.status != code {
if w.Written() {
debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
return
}
w.status = code
}
}Example Warning Output:
[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422
Proposed Solution
Add a similar warning in the Context.Render() method when attempting to write the response body after it has already been written.
Suggested Implementation Location
File: context.go:1126
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
// Add warning here
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 {
_ = c.Error(err)
c.Abort()
}
}Expected Warning Output
[GIN-debug] [WARNING] Response body already written. Attempting to write again with status code 200
Proof of Concept
I've verified this behavior with the following test:
func TestDemonstrateMultipleWriteIssue(t *testing.T) {
router := New()
router.Use(func(c *Context) {
// Middleware accidentally writes response
c.JSON(200, H{"middleware": "response"})
})
router.GET("/test", func(c *Context) {
// Handler also writes response
c.JSON(200, H{"handler": "response"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
router.ServeHTTP(w, req)
// Result: {"middleware":"response"}{"handler":"response"}
// Invalid JSON, no warning issued
}Affected Methods
All response writing methods in context.go would benefit from this warning:
c.JSON()(line 1177)c.IndentedJSON()(line 1156)c.SecureJSON()(line 1162)c.JSONP()(line 1171)c.AsciiJSON()(line 1183)c.PureJSON()(line 1189)c.XML()(line 1195)c.YAML()(line 1201)c.TOML()(line 1207)c.ProtoBuf()(line 1213)c.String()(line 1221)c.HTML()(line 1146)c.Data()(line 1227)c.DataFromReader()(line 1234)c.Redirect()(line 1296)
All these methods call c.Render() internally, so adding the warning in Render() would cover all cases.
Benefits
- Better Developer Experience: Immediate feedback when making this common mistake
- Consistency: Matches the existing warning pattern for header writes
- Easier Debugging: Makes it much easier to identify when middleware or handlers are incorrectly writing multiple responses
- Non-Breaking: This is a warning only, existing behavior remains unchanged
- Debug Mode Only: Uses
debugPrint()so it only appears in debug mode
Considerations
Backward Compatibility
- This change is non-breaking - it only adds warnings in debug mode
- The
TestMiddlewareWritetest currently expects multiple writes to succeed and concatenate - This test should continue to pass but would show warnings in debug output
Alternative Approaches
- Add warning to each response method individually - More verbose but allows method-specific messages
- Add a context flag to track first write - Could provide more detailed information about what was written first
- Make it configurable - Add an option to disable the warning if intentional multiple writes are needed
Environment
- Gin Version: master branch
- Go Version: 1.24+