Compare commits

...

4 Commits

Author SHA1 Message Date
OHZEKI Naoki
26a04962f4
Merge bf0836cc3a34af65b42744d2bc39d65df878ae41 into 5fad976b372e381312f8de69f0969f1284d229d3 2025-11-16 13:39:40 +05:30
Pawan Kalyan
5fad976b37
fix(gin): literal colon routes not working with engine.Handler() (#4415)
* fix: call updateRouteTrees in ServeHTTP using sync.Once to support literal colon routes in all usage scenarios (#4413)

* chore: fixed golangci-lint issue in test cases for literal colon

* fix: gofumpt formatting issue

* fix: gofumpt issue in gin.go

* chore: updated routeTreesUpdated comments

* chore: removed unused variable and updated TestUpdateRouteTreesCalledOnce testcase

* chore: moved tests from literal_colon_test.go into gin_test.go

---------

Co-authored-by: pawannn <pawan@zenz.tech>
2025-11-16 09:22:07 +08:00
OHZEKI Naoki
bf0836cc3a test(debug): improve tests 2025-10-22 22:40:26 +09:00
OHZEKI Naoki
cfeb557b54 fix(debug): fix version mismatch 2025-10-22 22:07:19 +09:00
4 changed files with 134 additions and 13 deletions

View File

@ -13,7 +13,9 @@ import (
"sync/atomic"
)
const ginSupportMinGoVer = 23
const ginSupportMinGoVer = 24
var runtimeVersion = runtime.Version()
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -77,7 +79,7 @@ func getMinVer(v string) (uint64, error) {
}
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
if v, e := getMinVer(runtimeVersion); e == nil && v < ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.24+.
`)

View File

@ -12,7 +12,6 @@ import (
"log"
"net/http"
"os"
"runtime"
"strings"
"sync"
"testing"
@ -21,10 +20,6 @@ import (
"github.com/stretchr/testify/require"
)
// TODO
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
// func debugPrint(format string, values ...any) {
func TestIsDebugging(t *testing.T) {
SetMode(DebugMode)
assert.True(t, IsDebugging())
@ -48,6 +43,18 @@ func TestDebugPrint(t *testing.T) {
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)
@ -104,12 +111,17 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
debugPrintWARNINGDefault()
SetMode(TestMode)
})
m, e := getMinVer(runtime.Version())
if e == nil && m < ginSupportMinGoVer {
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)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
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) {

8
gin.go
View File

@ -98,6 +98,10 @@ const (
type Engine struct {
RouterGroup
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times concurrently.
routeTreesUpdated sync.Once
// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
@ -654,6 +658,10 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.routeTreesUpdated.Do(func() {
engine.updateRouteTrees()
})
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req

View File

@ -913,3 +913,102 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
assert.NotPanics(t, func() { g.ServeHTTP(resp, req) })
assert.Equal(t, http.StatusNotFound, resp.Code)
}
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
func TestLiteralColonWithRun(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.updateRouteTrees()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithDirectServeHTTP(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHandler(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
handler := router.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
}
func TestLiteralColonWithHTTPServer(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.JSON(http.StatusOK, H{"path": "literal_colon"})
})
router.GET("/test/:param", func(c *Context) {
c.JSON(http.StatusOK, H{"param": c.Param("param")})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "literal_colon")
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest(http.MethodGet, "/test/foo", nil)
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
assert.Contains(t, w2.Body.String(), "foo")
}
// Test that updateRouteTrees is called only once
func TestUpdateRouteTreesCalledOnce(t *testing.T) {
SetMode(TestMode)
router := New()
router.GET(`/test\:action`, func(c *Context) {
c.String(http.StatusOK, "ok")
})
for range 5 {
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/test:action", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ok", w.Body.String())
}
}