6

I have been using golang's default http.ServeMux for http route handling.

wrap := func(h func(t *MyStruct, w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        h(t, w, r)
    }
}

// Register handlers with default mux
httpMux := http.NewServeMux()
httpMux.HandleFunc("/", wrap(payloadHandler))

Assume this server is accessible via http://example.com/

Very few of my client's requests were of path http://example.com/api//module (note the extra slash) which is redirected as 301 Moved Permanently. Exploring inside golang's http ServeMux.Handler(r *Request) function, seems it's intended.

path := cleanPath(r.URL.Path)
// if there is any change between actual and cleaned path, the request is 301 ed
if path != r.URL.Path {
    _, pattern = mux.handler(host, path)
    url := *r.URL
    url.Path = path
    return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}

I've looked into other similar issue.

go-web-server-is-automatically-redirecting-post-requests

Above qn has problem with redundant / in register pattern itself, but my use case is not with register pattern (in some nested path which is irrelevent to register pattern)

Problem is, since my client's requests are POST, browsers handle 301 with new GET request with exact query params and POST body. But change in the HTTP method causes the request to fail.

I have already instructed client to fix the redundant / in url, but the fix might take few (?) weeks to be deployed in all client locations.

Also these redundant / are handled fine in Apache Tomcat, but fails only in golang server. So is this the intended behaviour in my use case (redundant / in nested path) with golang or possible bug?

I am thinking of way to override the Handler func of ServeMux, but it won't be useful since Handler calls are made internally. Looking to disable this 301 behaviour, help would be appreciated.

Relevant links

http-post-method-is-actally-sending-a-get

The Coder
  • 2,562
  • 5
  • 33
  • 62

2 Answers2

8

The clean and redirect is intended behavior.

Wrap the mux with a handler that removes the double slashes:

type slashFix struct {
    mux http.Handler
}

func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    r.URL.Path = strings.Replace(r.URL.Path, "//", "/", -1)
    h.mux.ServeHTTP(w, r)
}

Use it like this:

httpMux := http.NewServeMux()
httpMux.HandleFunc("/", wrap(payloadHandler))
http.ListenAndServe(addr, &slashFix{httpMux})
Charlie Tumahai
  • 113,709
  • 12
  • 249
  • 242
  • First I spiked with Gorilla Mux, but after seeing this.. Made my day..! – The Coder Nov 24 '17 at 16:14
  • To see how this works https://go.dev/play/p/R58QfhZtgyL – Ryan Schumacher Mar 08 '23 at 11:25
  • When using `http.ServeMux`, it's important to note that `type slashFix struct { mux http.Handler }` is not the same as `type slashFix struct { mux *http.Handler }`. You will need to use `*http.ServeMux` as your type. – Ryan Schumacher Mar 22 '23 at 15:56
  • @RyanSchumacher the answer stores a `*http.SereveMux` in a field with interface type `http.Handler`. The answer does not use or suggest the use of the type `*http.Handler`. – Charlie Tumahai Mar 22 '23 at 21:57
  • @CeriseLimón my comment does not suggest that your answer is wrong. It seems like a very easy user error. I'm providing a suggestion to other developers that may make the same mistake I made. – Ryan Schumacher Mar 23 '23 at 22:07
1

Accepeted answer solved the problem

One more way is to use Gorilla mux and setting SkipClean(true). But be sure to know about the side effects in its doc

SkipClean defines the path cleaning behaviour for new routes. The initial value is false. Users should be careful about which routes are not cleaned. When true, if the route path is "/path//to", it will remain with the double slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/

When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will become /fetch/http/xkcd.com/534

func (r *Router) SkipClean(value bool) *Router {
    r.skipClean = value
    return r
}
The Coder
  • 2,562
  • 5
  • 33
  • 62