- 
                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