gin/debug_test.go
İsmail Daşcı fc23a46a1e feat: add warning for multiple response body writes
Add a debug warning when attempting to write to the response body
multiple times. This helps developers identify subtle bugs where
middleware or handlers inadvertently write responses multiple times,
resulting in invalid output (e.g., concatenated JSON objects).

This change:
- Adds a warning in Context.Render() when Writer.Written() is true
- Follows the existing pattern used for header write warnings
- Only shows in debug mode via debugPrint()
- Is non-breaking and backward compatible

Fixes #4477

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 20:41:36 +03:00

198 lines
5.7 KiB
Go

// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"errors"
"fmt"
"html/template"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsDebugging(t *testing.T) {
SetMode(DebugMode)
assert.True(t, IsDebugging())
SetMode(ReleaseMode)
assert.False(t, IsDebugging())
SetMode(TestMode)
assert.False(t, IsDebugging())
}
func TestDebugPrint(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
SetMode(ReleaseMode)
debugPrint("DEBUG this!")
SetMode(TestMode)
debugPrint("DEBUG this!")
SetMode(DebugMode)
debugPrint("these are %d %s", 2, "error messages")
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
}
func TestDebugPrintFunc(t *testing.T) {
DebugPrintFunc = func(format string, values ...any) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrint("debug print func test: %d", 123)
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] debug print func test: 123`, re)
}
func TestDebugPrintError(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintError(nil)
debugPrintError(errors.New("this is an error"))
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re)
}
func TestDebugPrintRoutes(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute(http.MethodGet, "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
func TestDebugPrintRouteFunc(t *testing.T) {
DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
fmt.Fprintf(DefaultWriter, "[GIN-debug] %-6s %-40s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute(http.MethodGet, "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
func TestDebugPrintLoadTemplate(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
debugPrintLoadTemplate(templ)
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re)
}
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGSetHTMLTemplate()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re)
}
func TestDebugPrintWARNINGDefault(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGDefault()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
func TestDebugPrintWARNINGDefaultWithUnsupportedVersion(t *testing.T) {
runtimeVersion = "go1.23.12"
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGDefault()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.24+.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
func TestDebugPrintWARNINGNew(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGNew()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re)
}
func captureOutput(t *testing.T, f func()) string {
reader, writer, err := os.Pipe()
if err != nil {
panic(err)
}
defaultWriter := DefaultWriter
defaultErrorWriter := DefaultErrorWriter
defer func() {
DefaultWriter = defaultWriter
DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr)
}()
DefaultWriter = writer
DefaultErrorWriter = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
var buf strings.Builder
wg.Done()
_, err := io.Copy(&buf, reader)
assert.NoError(t, err)
out <- buf.String()
}()
wg.Wait()
f()
writer.Close()
return <-out
}
func TestGetMinVer(t *testing.T) {
var m uint64
var e error
_, e = getMinVer("go1")
require.Error(t, e)
m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m)
require.NoError(t, e)
m, e = getMinVer("go1.1.1")
require.NoError(t, e)
assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1")
require.Error(t, e)
}
func TestRenderWarnsOnMultipleWrites(t *testing.T) {
re := captureOutput(t, func() {
SetMode(DebugMode)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
// First write
c.JSON(http.StatusOK, H{"first": "response"})
// Second write should trigger warning
c.JSON(http.StatusOK, H{"second": "response"})
SetMode(TestMode)
})
assert.Contains(t, re, "[WARNING] Response body already written")
}