Dealing with Trailing Slashes on RequestURI in Go with Mux

23. March 2018 Blog 6

I recently was building a REST API with Go and I utilized gorilla/mux for routing. I quickly realized that if you create a router handler it doesn’t handle trailing slashes. So if you write code like this:

router.HandleFunc("/users",GetList).Methods("GET")

It would get would handle requests to /users but not /users/. So I did a little bit of research and sure enough Mux has an option to handle this called StrictSlashes. If you enable this feature then it redirects routes without a trailing slash to a route with a trailing slash, it did exactly what I needed. That is until I got to a POST request. If you read the documentation for ScrictSlashes it lets you know that it generates a 301 redirect and converts all requests to GET requests. So my POST to /users was turning into a GET to /users/.

So I figured I would try to write some middleware for Mux that would handle this. However, middleware for Mux doesn’t fire till after it finds the handler for the route, so once again no go. Finally, I tried writing a http Handler outside of Mux. This is what I ended up with and it works great.

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/users",GetList).Methods("GET")
    router.HandleFunc("/users",PostInsert).Methods("POST")
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d",port), loggingMiddleware(router)))
}

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
        // Do stuff here
        log.Println(r.RequestURI)
        // Call the next handler, which can be another middleware in the chain, or the final handler.
        next.ServeHTTP(w, r)
    })
}

On my first attempt I tried just modifying r.RequestURI however, I figured out Mux actually uses r.URL.PATH when matching routes. So all this function does is trims off any trailing slashes. So now you just need a router handler for /users and it will handle requests to /users/ and it works for any type of requests GET, POST, PUT, etc…


6 thoughts on “Dealing with Trailing Slashes on RequestURI in Go with Mux”

  • 1
    Gavin on May 15, 2018 Reply

    You just saved me a lot of headache. Thanks!

  • 2
    Frederik Vosberg on July 24, 2018 Reply

    Hi, thanks for this thoughts. The solution is good and it saves others from having to go the same learning path, as you did. One thing I would like to mention: If you just read the main function, you won’t expect something like a loggingMiddleware to change anything. So I suggest to have two middlewares like

    http.ListenAndServe(port, loggingMiddleware(trailingSlashesMiddleware(router)))

    So if anything goes wrong according to routing, you know you might want to have a look at this middleware, in particular when you define a Route with a trailing slash, which would never match.

    • 3
      pitchinnate on August 15, 2018 Reply

      Correct I agree, I did end up making it into it’s own middleware. Thanks for your feedback.

  • 4
    Shane on January 31, 2019 Reply

    Hi Nate,
    I sent you an email. But maybe I should have just commented here. I am trying to implement your loggingMiddleware func but my POST /endpoint/ still does a GET instead of a POST. Here’s my code…

    package router

    import (
    “net/http”
    “strings”

    “github.com/gorilla/mux”
    )

    // Routes is a slice of Route
    type Routes []Route

    // Route defines a route
    type Route struct {
    Name string
    Method string
    Pattern string
    HandlerFunc http.HandlerFunc
    }

    // This is a sample of Routes which you must define in your code
    // var routes = route.Routes{
    // route.Route{
    // Name: “GetChoreographies”,
    // Method: “GET”,
    // Pattern: “/choreography”,
    // HandlerFunc: controller.GetChoreographies,
    // },
    // route.Route{
    // Name: “GetChoreographyById”,
    // Method: “GET”,
    // Pattern: “/choreography/{tenantId}”,
    // HandlerFunc: controller.GetChoreographyById,
    // },

    // NewRouter configures a new router to the API
    func NewRouter(routes Routes) *mux.Router {
    router := mux.NewRouter().StrictSlash(true)
    for _, route := range routes {
    var handler http.Handler
    handler = route.HandlerFunc
    handler = logger(handler, route.Name)
    router.
    Methods(route.Method).
    Path(route.Pattern).
    Name(route.Name).
    Handler(loggingMiddleware(handler))
    }
    return router
    }

    func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    r.URL.Path = strings.TrimSuffix(r.URL.Path, “/”)
    next.ServeHTTP(w, r)
    })
    }

    • 5
      Shane on January 31, 2019 Reply

      So, my issue was the misplacement of the trim for the URL.Path. I needed outside of mux and actually on the ListenAndServe. I was missing that from the example above. I have it working now. Nate sidebared with me through email. Thank you.

      func (a *API) run(addr string) {
      // these two lines are important in order to allow access from the front-end side to the methods
      allowedOrigins := handlers.AllowedOrigins([]string{“*”})
      allowedMethods := handlers.AllowedMethods([]string{“GET”, “POST”, “DELETE”, “PUT”, “OPTONS”})

      // launch server with CORS validations
      err := http.ListenAndServe(addr, handlers.CORS(allowedOrigins, allowedMethods)(stripTrailingSlashes(a.router)))

      if err != nil {
      log.Error(“ListenAndServe”,
      zap.String(“error”, err.Error()),
      )
      }
      }

      // Needed this to trim trailing slashes on POST methods. StrictSlash isn’t working correctly and was redirecting POSTs to GETs
      // when a trailing slash was added
      func stripTrailingSlashes(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      r.URL.Path = strings.TrimSuffix(r.URL.Path, “/”)
      next.ServeHTTP(w, r)
      })
      }

  • 6
    Pete on July 31, 2019 Reply

    This gave me some trouble with the “/” route redirecting infinitely. I eventually updated it to this:

    https://gist.github.com/l3njo/bf3fad38329732fd665ad58fa1eb1866

Leave a Reply to Gavin Cancel reply

Your email address will not be published. Required fields are marked *