Skip to content

Add warning for multiple response writes #4477

@raju-mechatronics

Description

@raju-mechatronics

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

  1. Silent Failures: Developers don't realize they're writing the response multiple times until they inspect the actual HTTP response
  2. Invalid Output: Multiple JSON/XML responses concatenated together produce invalid output
  3. Difficult Debugging: This issue often occurs across middleware and handlers, making it hard to track down
  4. 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

  1. Better Developer Experience: Immediate feedback when making this common mistake
  2. Consistency: Matches the existing warning pattern for header writes
  3. Easier Debugging: Makes it much easier to identify when middleware or handlers are incorrectly writing multiple responses
  4. Non-Breaking: This is a warning only, existing behavior remains unchanged
  5. 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 TestMiddlewareWrite test currently expects multiple writes to succeed and concatenate
  • This test should continue to pass but would show warnings in debug output

Alternative Approaches

  1. Add warning to each response method individually - More verbose but allows method-specific messages
  2. Add a context flag to track first write - Could provide more detailed information about what was written first
  3. 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+

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/proposalGot an idea for a feature that Gin doesn't have currently? Submit your idea here!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions