Dealing with Trailing Slashes on RequestURI in Go with Mux
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…
You just saved me a lot of headache. Thanks!
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.
Correct I agree, I did end up making it into it’s own middleware. Thanks for your feedback.
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)
})
}
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)
})
}
This gave me some trouble with the “/” route redirecting infinitely. I eventually updated it to this:
https://gist.github.com/l3njo/bf3fad38329732fd665ad58fa1eb1866
I saw another solution to use a regex in the pattern to match with and without the trailing slash:
https://github.com/gorilla/mux/issues/30#issuecomment-318297249
router.HandleFunc(“/{users:users\\/?}”, handler)
will handle /users as well as /users/