From 3af72917180aeb2c28f334fbb366b2e82f35c916 Mon Sep 17 00:00:00 2001 From: mehrdadbn9 Date: Fri, 6 Feb 2026 14:35:21 +0330 Subject: [PATCH 1/2] Fix concurrent-safe route registration Add sync.RWMutex to protect concurrent access to the trees slice in Engine struct. This fixes a data race between addRoute() and Routes() functions where one goroutine appends to trees while another reads from it. Changes: - Add mu sync.RWMutex field to Engine struct - Protect addRoute() with Lock/Unlock - Protect Routes() with RLock/RUnlock - Add TestConcurrentAddRouteAndRoutes to test concurrent access Fixes #4457 --- gin.go | 9 +++++++++ gin_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/gin.go b/gin.go index 2e033bf3..472c9ec8 100644 --- a/gin.go +++ b/gin.go @@ -96,6 +96,10 @@ type Engine struct { // (used for routing HTTP requests) happens only once, even if called multiple times concurrently. routeTreesUpdated sync.Once + + // mu protects concurrent access to trees + mu sync.RWMutex + // 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 @@ -368,6 +372,9 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { debugPrintRoute(method, path, handlers) + engine.mu.Lock() + defer engine.mu.Unlock() + root := engine.trees.get(method) if root == nil { root = new(node) @@ -388,6 +395,8 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { // Routes returns a slice of registered routes, including some useful information, such as: // the http method, path, and the handler name. func (engine *Engine) Routes() (routes RoutesInfo) { + engine.mu.RLock() + defer engine.mu.RUnlock() for _, tree := range engine.trees { routes = iterate("", tree.method, routes, tree.root) } diff --git a/gin_test.go b/gin_test.go index 43c9494d..c69ab0b0 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1067,6 +1067,39 @@ func TestLiteralColonWithHTTPServer(t *testing.T) { assert.Contains(t, w2.Body.String(), "foo") } +func TestConcurrentAddRouteAndRoutes(t *testing.T) { + router := New() + + done := make(chan bool) + + for i := 0; i < 10; i++ { + go func(n int) { + router.GET(fmt.Sprintf("/route%d", n), func(c *Context) { + c.String(http.StatusOK, fmt.Sprintf("route%d", n)) + }) + router.POST(fmt.Sprintf("/route%d", n), func(c *Context) { + c.String(http.StatusOK, fmt.Sprintf("route%d", n)) + }) + done <- true + }(i) + } + + for i := 0; i < 10; i++ { + go func() { + _ = router.Routes() + done <- true + }() + } + + for i := 0; i < 20; i++ { + <-done + } + + routes := router.Routes() + assert.Len(t, routes, 20) +} + + // Test that updateRouteTrees is called only once func TestUpdateRouteTreesCalledOnce(t *testing.T) { SetMode(TestMode) From 8f39ea663a8ef23001ee7b30e8d66f63367bc1a7 Mon Sep 17 00:00:00 2001 From: mehrdadbn9 Date: Fri, 6 Feb 2026 14:39:34 +0330 Subject: [PATCH 2/2] Fix formatting in test file --- gin_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gin_test.go b/gin_test.go index c69ab0b0..f2417689 100644 --- a/gin_test.go +++ b/gin_test.go @@ -1099,7 +1099,6 @@ func TestConcurrentAddRouteAndRoutes(t *testing.T) { assert.Len(t, routes, 20) } - // Test that updateRouteTrees is called only once func TestUpdateRouteTreesCalledOnce(t *testing.T) { SetMode(TestMode)