fix: flush status immediately for no-body response codes

ctx.Status() sets the status on gin's responseWriter but never flushes
it to the underlying http.ResponseWriter when no body is written. This
causes httptest.ResponseRecorder and similar wrappers to see the default
200 instead of the actual status code.

This particularly affects status codes that never have a body (1xx, 204,
304) where handlers typically call ctx.Status() without writing content.

The fix mirrors the existing behavior in ctx.Render(), which already
calls WriteHeaderNow() for no-body status codes. By flushing immediately
for these codes in Status(), the correct status propagates to the
underlying writer without requiring a separate Write() call.

Fixes #4071
This commit is contained in:
xbrxr03 2026-06-22 11:30:38 -04:00
parent da1e108614
commit 065f526e70
2 changed files with 60 additions and 0 deletions

View File

@ -1107,8 +1107,15 @@ func bodyAllowedForStatus(status int) bool {
}
// Status sets the HTTP response code.
// For status codes that do not allow a response body (1xx, 204, 304),
// the status is flushed immediately so that httptest.ResponseRecorder
// and similar wrappers record the correct status without requiring a
// subsequent Write call.
func (c *Context) Status(code int) {
c.Writer.WriteHeader(code)
if !bodyAllowedForStatus(code) {
c.Writer.WriteHeaderNow()
}
}
// Header is an intelligent shortcut for c.Writer.Header().Set(key, value).

View File

@ -1898,6 +1898,59 @@ func TestContextAbortWithStatus(t *testing.T) {
assert.True(t, c.IsAborted())
}
func TestContextStatusFlushesNoBodyCodes(t *testing.T) {
t.Run("204 No Content is flushed immediately", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Status(http.StatusNoContent)
assert.Equal(t, http.StatusNoContent, c.Writer.Status())
assert.Equal(t, http.StatusNoContent, w.Code, "Status(204) should flush to underlying ResponseWriter without requiring Write()")
})
Run := func(code int) {
t.Run(fmt.Sprintf("%d %s is flushed immediately", code, http.StatusText(code)), func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Status(code)
assert.Equal(t, code, c.Writer.Status())
assert.Equal(t, code, w.Code, "Status(%d) should flush to underlying ResponseWriter", code)
})
}
// 1xx informational codes
Run(http.StatusContinue) // 100
Run(http.StatusSwitchingProtocols) // 101
Run(http.StatusProcessing) // 102
// 204 No Content
Run(http.StatusNoContent)
// 304 Not Modified
Run(http.StatusNotModified)
}
func TestContextStatusWithBodyDoesNotFlushEarly(t *testing.T) {
// Status codes that allow a body (e.g. 200) should NOT flush immediately,
// because the handler may still write content. The status is flushed
// when Write() is called.
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Status(http.StatusOK)
assert.Equal(t, http.StatusOK, c.Writer.Status())
// w.Code is 200 (default) because WriteHeaderNow has been called with the
// default status. The actual written status should still be 200 after
// Status(200) since no content has been written yet, but the key point
// is that calling Status(200) alone should not force an early flush
// that would prevent setting headers afterward.
assert.Equal(t, http.StatusOK, w.Code)
}
type testJSONAbortMsg struct {
Foo string `json:"foo"`
Bar string `json:"bar"`