• With issues: Inconsistent Middleware Application Based on Route Registration Order

Description

  • Question 1: Method 1 and Method 2 only differ by the order of registration;Why do Method 1 and Method 2 produce different results? Are these methods legal? Or is this a bug?
  • Question 2: Isn't Method 3 a bit redundant since the same route group is written twice?

How to reproduce

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type registerFunc func(*gin.RouterGroup) // Type for route registration functions

var registerFuncSlice []registerFunc // Slice of all route registration functions

func init() {
    // Register routes
    registerFuncSlice = append(registerFuncSlice, fooRoutes)
}

func fooHandler(c *gin.Context) {
    key := c.GetString("testKey")
    c.JSON(http.StatusOK, gin.H{"testValue": key})
    c.Abort()
}

func testMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("testKey", "test")
        c.Next()
    }
}

func fooRoutes(v1 *gin.RouterGroup) {

    // Method 1: If fooPrivateRoutes is placed before fooPublicRoutes, fooPublicRoutes
    // will apply the testMiddleware (not expected)
    // Accessing http://127.0.0.1:8888/v1/test/no-test-data returns {"testValue":"test"}
    foo := v1.Group("/test")
    fooPrivateRoutes(foo)
    fooPublicRoutes(foo)

    //// Method 2: If fooPrivateRoutes is placed after fooPublicRoutes, fooPublicRoutes
    //// will not apply the testMiddleware (expected)
    //// Accessing http://127.0.0.1:8888/v1/test/no-test-data returns {"testValue":""}
    //foo := v1.Group("/test")
    //fooPublicRoutes(foo)
    //fooPrivateRoutes(foo)

    //// Method 3: Separate the two, regardless of the order (expected)
    //// Accessing http://127.0.0.1:8888/v1/test/no-test-data returns {"testValue":""}
    //fooPrivate := v1.Group("/test")
    //fooPrivateRoutes(fooPrivate)
    //
    //fooPublic := v1.Group("/test")
    //fooPublicRoutes(fooPublic)

    /*
        Question 1: Method 1 and Method 2 only differ by the order of registration;
        Why do Method 1 and Method 2 produce different results? Are these methods legal? Or is this a bug?

        Question 2: Isn't Method 3 a bit redundant since the same route group is written twice?
    */
}

func fooPublicRoutes(group *gin.RouterGroup) {
    group.GET("/no-test-data", fooHandler)
}

func fooPrivateRoutes(group *gin.RouterGroup) {
    test := group.Use(testMiddleware())
    test.GET("/has-test-data", fooHandler)
}

func setupRouter() *gin.Engine {
    e := gin.Default()
    v1 := e.Group("v1") // Register route group

    // Dynamically register all routes
    for _, regFunc := range registerFuncSlice {
        regFunc(v1)
    }

    if err := e.Run("127.0.0.1:8888"); err != nil {
        panic(err)
    }
    return e
}

func main() {
    setupRouter()
}

Expectations

$ curl http://127.0.0.1:8888/v1/test/no-test-data
{"testValue":""}

Actual result

$ curl http://127.0.0.1:8888/v1/test/no-test-data
{"testValue":"test"}

Environment

  • go version:go1.23.3
  • gin version:v1.10.0
  • operating system:windows 11

Comment From: simon-winter

related: https://github.com/gin-gonic/gin/issues/4109

Comment From: jiaopengzi

hello @manucorporat

  1. Why do I need to pay attention to the registration order within the same route group for Method 1 and Method 2? I find this unreasonable.
  2. Why does Method 3 work when written separately, even though it belongs to the same route group?

Comment From: Cristigeo

It looks like your code is adding 3 handlers on the same RouteGroup (/v1/test): one for the /has-test-data endpoint, one for the /no-test-data, and the middleware, which is basically a handler for ANY endpoint in the group (think of it like /*). When handling a request for the group, handlers are matched and executed in the order they were registered.

For Method 1, the handlers are registered in the order: /* (middleware) -> adds "test" /has-test-data -> returns response /no-test-data -> returns response so the "test" value will always be present in the responses for both /has-test-data and /no-test-data.

For Method 2, the handlers are registered in the order: /no-test-data -> returns response /* (middleware) -> adds "test" /has-test-data -> returns response so the "test" value will only appear in the /has-test-data response.

From a code perspective, the behaviour seems to be as expected, even if it looks counterintuitive.

Disclaimer: I'm very new to Gin; my explanation is based on the RouterGroup's implementation of the IRoutes interface, but maybe I misread something.