From d3ffc9985281dcf4d3bef604cce4e662b1a327a6 Mon Sep 17 00:00:00 2001 From: Abhiyan Khanal <51784021+abhiyankhanal@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:34:59 +0545 Subject: [PATCH] test(engine): add regression tests for HandleContext with NoRoute (#4571) Add two regression tests for GitHub issue #1848 to verify that Context.handlers are properly isolated in HandleContext when used from a NoRoute handler with group and engine middleware. Co-authored-by: Claude Opus 4.6 Co-authored-by: Bo-Yi Wu --- gin_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/gin_test.go b/gin_test.go index 43c9494d..a9cf1755 100644 --- a/gin_test.go +++ b/gin_test.go @@ -743,6 +743,78 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) { assert.Equal(t, int64(1), handlerCounterV2) } +func TestEngineHandleContextNoRouteWithGroupMiddleware(t *testing.T) { + // Scenario from issue #1848: + // - Engine with no global middleware (gin.New()) + // - A group with middleware + // - A route in that group + // - NoRoute handler that redirects via HandleContext + // The group middleware should run exactly once per HandleContext call, + // not accumulate across redirects. + + var middlewareCount, handlerCount int64 + + r := New() + grp := r.Group("", func(c *Context) { + atomic.AddInt64(&middlewareCount, 1) + c.Next() + }) + grp.GET("/target", func(c *Context) { + atomic.AddInt64(&handlerCount, 1) + c.String(http.StatusOK, "ok") + }) + + r.NoRoute(func(c *Context) { + c.Request.URL.Path = "/target" + r.HandleContext(c) + }) + + // Access a non-existent route to trigger NoRoute -> HandleContext + w := PerformRequest(r, "GET", "/nonexistent") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "ok", w.Body.String()) + // Middleware and handler should each run exactly once + assert.Equal(t, int64(1), atomic.LoadInt64(&middlewareCount)) + assert.Equal(t, int64(1), atomic.LoadInt64(&handlerCount)) +} + +func TestEngineHandleContextNoRouteWithEngineMiddleware(t *testing.T) { + // When engine middleware exists and NoRoute redirects via HandleContext, + // verify the handlers run the expected number of times. + + var engineMwCount, groupMwCount, handlerCount int64 + + r := New() + r.Use(func(c *Context) { + atomic.AddInt64(&engineMwCount, 1) + c.Next() + }) + + grp := r.Group("", func(c *Context) { + atomic.AddInt64(&groupMwCount, 1) + c.Next() + }) + grp.GET("/target", func(c *Context) { + atomic.AddInt64(&handlerCount, 1) + c.String(http.StatusOK, "ok") + }) + + r.NoRoute(func(c *Context) { + c.Request.URL.Path = "/target" + r.HandleContext(c) + }) + + w := PerformRequest(r, "GET", "/nonexistent") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "ok", w.Body.String()) + // Handler and group middleware should each run once (from HandleContext) + assert.Equal(t, int64(1), atomic.LoadInt64(&handlerCount)) + assert.Equal(t, int64(1), atomic.LoadInt64(&groupMwCount)) + // Engine middleware runs twice: once for the NoRoute chain, once for the HandleContext chain + // This is expected behavior since HandleContext re-enters the full handler chain + assert.Equal(t, int64(2), atomic.LoadInt64(&engineMwCount)) +} + func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) { r := New() r.UseEscapedPath = true