Search code examples
apigomux

Golang Api Only Matches Last Route


I have a golang api application. I've defined a set of routes and handlers. However, the mux router only ever returns the last route.

When I request /api/info I get this in my logging:

9:0:38 app | 2018/02/05 09:00:38 GET /api/info Users Create 308.132µs

Why is that routing to the wrong route?

routing package:

// NewRouter establishes the root application router
func NewRouter(context *config.ApplicationContext, routes Routes, notFoundHandler http.HandlerFunc) *mux.Router {
    router := mux.NewRouter()

    router.NotFoundHandler = notFoundHandler

    for _, route := range routes {
        router.
            PathPrefix("/api").
            Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            // TODO: fix HandlerFunc. Right now, it is overriding previous routes and setting a single handler for all
            // this means that the last route is the only router with a handler
            HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                logRoute(setJSONHeader(route.HandlerFunc), route.Name)(context, w, r)
            })

    }

    return router
}

func logRoute(inner ContextHandlerFunc, name string) ContextHandlerFunc {
    return func(c *config.ApplicationContext, w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        inner(c, w, r)

        log.Printf(
            "%s\t%s\t%s\t%s",
            r.Method,
            r.RequestURI,
            name,
            time.Since(start),
        )
    }
}

func setJSONHeader(inner ContextHandlerFunc) ContextHandlerFunc {
    return func(c *config.ApplicationContext, w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        inner(c, w, r)
    }
}

main package:

var context = config.ApplicationContext{
    Database: database.NewDatabase().Store,
}

var routes = router.Routes{
    router.Route{"Info", "GET", "/info", handlers.InfoShow},
    router.Route{"Users Create", "POST", "/users/create", handlers.UsersCreate},
}

func main() {    
    notFoundHandler := handlers.Errors404
    router := router.NewRouter(&context, routes, notFoundHandler)

    port := os.Getenv("PORT")

    log.Fatal(http.ListenAndServe(":"+port, router))
}

If I visit /api/info it will attempt to call a POST to /users/create. However, if I remove the second route, it will correctly route to the InfoShow handler.

Why is mux overriding the first route? I'm fairly certain there's something wrong with

HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    logRoute(setJSONHeader(route.HandlerFunc), route.Name)(context, w, r)
}) 

but I'm not sure why that would cause it to map over the first route.

Ideas?


Solution

  • Reading through your code and gorilla/mux, I think I know the issue. You're using the for loop variable route, and specifically its field HanderFunc, in the function literal, but because of how function literals work, the value of that field is not evaluated until the that function literal is called. In Go, the second variable in a range loop is reused on each iteration, rather than created anew, and so after the for loop, if it's still in scope of anything (like your function literal), it will contain the value of the last loop iteration. Here's an example of what I mean:

    https://play.golang.org/p/Xx62tuwhtgG

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        var funcs []func()
        ints := []int{1, 2, 3, 4, 5}
    
        // How you're doing it
        for i, a := range ints {
            fmt.Printf("Loop i: %v, a: %v\n", i, a)
            funcs = append(funcs, func() {
                fmt.Printf("Lambda i: %v, a: %v\n", i, a)
            })
        }
    
        for _, f := range funcs {
            f()
        }
    
        fmt.Println("-------------")
    
        // How you *should* do it
        funcs = nil
        for i, a := range ints {
            i := i
            a := a
            fmt.Printf("Loop i: %v, a: %v\n", i, a)
            funcs = append(funcs, func() {
                fmt.Printf("Lambda i: %v, a: %v\n", i, a)
            })
        }
    
        for _, f := range funcs {
            f()
        }
    }
    

    In the first example, i and a are being reused on each loop iteration, and aren't evaluated for their values in the lambda (function literal) until that lambda is actually called (by the funcs loop). To fix that, you can shadow a and i by redeclaring them inside the scope of the loop iteration (but outside the lambda's scope). This makes a separate copy for each iteration, to avoid issues with reuse of the same variable.

    Specifically for your code, if you change your code to the following, it should work:

    for _, route := range routes {
        route := route // make a copy of the route for use in the lambda
        // or alternatively, make scoped vars for the name and handler func
    
        router.
            PathPrefix("/api").
            Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            // TODO: fix HandlerFunc. Right now, it is overriding previous routes and setting a single handler for all
            // this means that the last route is the only router with a handler
            HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                logRoute(setJSONHeader(route.HandlerFunc), route.Name)(context, w, r)
            })
    }