-
Notifications
You must be signed in to change notification settings - Fork 8.5k
Description
Description
Gin currently supports literal colon routes (e.g., /api/v1\:method) through backslash escaping, as implemented in PR #2857. However, there's a significant usage limitation:
Current Behavior
engine.Run()✅ Works correctly (automatically callsupdateRouteTrees())engine.Handler()❌ Doesn't work (doesn't callupdateRouteTrees())- Direct usage as
http.Handler❌ Doesn't work
Root Cause
The literal colon feature depends on updateRouteTrees() method to convert stored escaped paths (\:) to actual paths (:). However, this method is only called in engine.Run(), not in other usage scenarios.
Problem Scenario
I encountered this issue when using the following code:
server := &http.Server{Handler: engine}
server.ListenAndServe()Potential Solutions
-
Use sync.Once in ServeHTTP:
var routeTreesUpdated sync.Once func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { routeTreesUpdated.Do(func() { engine.updateRouteTrees() }) // existing logic... }
-
Add updateRouteTrees() logic in engine.Handler():
Recommend users to useengine.Handler()instead of using engine directly, and callupdateRouteTrees()in Handler() method -
Update existing colon implementation:
Redesign the literal colon handling mechanism to avoid the need for delayed route tree updates
Gin Version
v1.11.0
Can you reproduce the bug?
Yes
Source Code
// Apology
// Due to continuous build failures on https://go.dev/play/ (dependency download timeout), I cannot provide runnable code snippets, but the test program has been uploaded to: https://go.dev/play/p/UJUroSOmkq1
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func setupRouter() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"type": "static",
"path": "/test",
})
})
r.GET("/test/:param", func(c *gin.Context) {
param := c.Param("param")
c.JSON(http.StatusOK, gin.H{
"type": "param",
"param": param,
})
})
r.GET(`/test\:action`, func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"type": "literal_colon",
"path": "/test:action",
})
})
r.UpdateRouteTrees() // important
return r
}
func TestRoutes(t *testing.T) {
router := setupRouter()
tests := []struct {
name string
url string
expectedCode int
expectedBody string
}{
{
name: "static route",
url: "/test",
expectedCode: http.StatusOK,
expectedBody: `{"path":"/test","type":"static"}`,
},
{
name: "param route",
url: "/test/foo",
expectedCode: http.StatusOK,
expectedBody: `{"param":"foo","type":"param"}`,
},
{
name: "literal colon route",
url: "/test:action",
expectedCode: http.StatusOK,
expectedBody: `{"path":"/test:action","type":"literal_colon"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", tt.url, nil)
router.ServeHTTP(w, req)
assert.Equal(t, tt.expectedCode, w.Code)
assert.JSONEq(t, tt.expectedBody, w.Body.String())
})
}
}
-- go.mod --
module gin_colon
go 1.25.3
require (
github.com/gin-gonic/gin v1.11.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/gin-gonic/gin => github.com/Zhang-Siyang/gin v0.0.0-20251030080216-1440601f40e6Go Version
1.25
Operating System
macOS/Linux