Skip to content

Commit ab40107

Browse files
namusyakatombergan
authored andcommitted
net/http: make ServeMux preserve query string during redirects
Ensure that the implicitly created redirect for "/route" after "/route/" has been registered doesn't lose the query string information. A previous attempt (https://golang.org/cl/43779) changed http.Redirect, however, that change broke direct calls to http.Redirect. To avoid that problem, this change touches ServeMux.Handler only. Fixes #17841 Change-Id: I303c1b1824615304ae68147e254bb41b0ea339be Reviewed-on: https://go-review.googlesource.com/61210 Run-TryBot: Emmanuel Odeke <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Tom Bergan <[email protected]> Reviewed-by: Emmanuel Odeke <[email protected]>
1 parent 5b043ab commit ab40107

File tree

2 files changed

+105
-22
lines changed

2 files changed

+105
-22
lines changed

src/net/http/serve_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,68 @@ func TestMuxRedirectLeadingSlashes(t *testing.T) {
461461
}
462462
}
463463

464+
// Test that the special cased "/route" redirect
465+
// implicitly created by a registered "/route/"
466+
// properly sets the query string in the redirect URL.
467+
// See Issue 17841.
468+
func TestServeWithSlashRedirectKeepsQueryString(t *testing.T) {
469+
setParallel(t)
470+
defer afterTest(t)
471+
472+
writeBackQuery := func(w ResponseWriter, r *Request) {
473+
fmt.Fprintf(w, "%s", r.URL.RawQuery)
474+
}
475+
476+
mux := NewServeMux()
477+
mux.HandleFunc("/testOne", writeBackQuery)
478+
mux.HandleFunc("/testTwo/", writeBackQuery)
479+
mux.HandleFunc("/testThree", writeBackQuery)
480+
mux.HandleFunc("/testThree/", func(w ResponseWriter, r *Request) {
481+
fmt.Fprintf(w, "%s:bar", r.URL.RawQuery)
482+
})
483+
484+
ts := httptest.NewServer(mux)
485+
defer ts.Close()
486+
487+
tests := [...]struct {
488+
path string
489+
method string
490+
want string
491+
statusOk bool
492+
}{
493+
0: {"/testOne?this=that", "GET", "this=that", true},
494+
1: {"/testTwo?foo=bar", "GET", "foo=bar", true},
495+
2: {"/testTwo?a=1&b=2&a=3", "GET", "a=1&b=2&a=3", true},
496+
3: {"/testTwo?", "GET", "", true},
497+
4: {"/testThree?foo", "GET", "foo", true},
498+
5: {"/testThree/?foo", "GET", "foo:bar", true},
499+
6: {"/testThree?foo", "CONNECT", "foo", true},
500+
7: {"/testThree/?foo", "CONNECT", "foo:bar", true},
501+
502+
// canonicalization or not
503+
8: {"/testOne/foo/..?foo", "GET", "foo", true},
504+
9: {"/testOne/foo/..?foo", "CONNECT", "404 page not found\n", false},
505+
}
506+
507+
for i, tt := range tests {
508+
req, _ := NewRequest(tt.method, ts.URL+tt.path, nil)
509+
res, err := ts.Client().Do(req)
510+
if err != nil {
511+
continue
512+
}
513+
slurp, _ := ioutil.ReadAll(res.Body)
514+
res.Body.Close()
515+
if !tt.statusOk {
516+
if got, want := res.StatusCode, 404; got != want {
517+
t.Errorf("#%d: Status = %d; want = %d", i, got, want)
518+
}
519+
}
520+
if got, want := string(slurp), tt.want; got != want {
521+
t.Errorf("#%d: Body = %q; want = %q", i, got, want)
522+
}
523+
}
524+
}
525+
464526
func BenchmarkServeMux(b *testing.B) {
465527

466528
type test struct {

src/net/http/server.go

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,9 +2112,8 @@ type ServeMux struct {
21122112
}
21132113

21142114
type muxEntry struct {
2115-
explicit bool
2116-
h Handler
2117-
pattern string
2115+
h Handler
2116+
pattern string
21182117
}
21192118

21202119
// NewServeMux allocates and returns a new ServeMux.
@@ -2192,6 +2191,31 @@ func (mux *ServeMux) match(path string) (h Handler, pattern string) {
21922191
return
21932192
}
21942193

2194+
// redirectToPathSlash determines if the given path needs appending "/" to it.
2195+
// This occurs when a handler for path + "/" was already registered, but
2196+
// not for path itself. If the path needs appending to, it creates a new
2197+
// URL, setting the path to u.Path + "/" and returning true to indicate so.
2198+
func (mux *ServeMux) redirectToPathSlash(path string, u *url.URL) (*url.URL, bool) {
2199+
if !mux.shouldRedirect(path) {
2200+
return u, false
2201+
}
2202+
path = path + "/"
2203+
u = &url.URL{Path: path, RawQuery: u.RawQuery}
2204+
return u, true
2205+
}
2206+
2207+
// shouldRedirect reports whether the given path should be redirected to
2208+
// path+"/". This should happen if a handler is registered for path+"/" but
2209+
// not path -- see comments at ServeMux.
2210+
func (mux *ServeMux) shouldRedirect(path string) bool {
2211+
if _, exist := mux.m[path]; exist {
2212+
return false
2213+
}
2214+
n := len(path)
2215+
_, exist := mux.m[path+"/"]
2216+
return n > 0 && path[n-1] != '/' && exist
2217+
}
2218+
21952219
// Handler returns the handler to use for the given request,
21962220
// consulting r.Method, r.Host, and r.URL.Path. It always returns
21972221
// a non-nil handler. If the path is not in its canonical form, the
@@ -2211,13 +2235,27 @@ func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
22112235

22122236
// CONNECT requests are not canonicalized.
22132237
if r.Method == "CONNECT" {
2238+
// If r.URL.Path is /tree and its handler is not registered,
2239+
// the /tree -> /tree/ redirect applies to CONNECT requests
2240+
// but the path canonicalization does not.
2241+
if u, ok := mux.redirectToPathSlash(r.URL.Path, r.URL); ok {
2242+
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
2243+
}
2244+
22142245
return mux.handler(r.Host, r.URL.Path)
22152246
}
22162247

22172248
// All other requests have any port stripped and path cleaned
22182249
// before passing to mux.handler.
22192250
host := stripHostPort(r.Host)
22202251
path := cleanPath(r.URL.Path)
2252+
2253+
// If the given path is /tree and its handler is not registered,
2254+
// redirect for /tree/.
2255+
if u, ok := mux.redirectToPathSlash(path, r.URL); ok {
2256+
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
2257+
}
2258+
22212259
if path != r.URL.Path {
22222260
_, pattern = mux.handler(host, path)
22232261
url := *r.URL
@@ -2273,35 +2311,18 @@ func (mux *ServeMux) Handle(pattern string, handler Handler) {
22732311
if handler == nil {
22742312
panic("http: nil handler")
22752313
}
2276-
if mux.m[pattern].explicit {
2314+
if _, exist := mux.m[pattern]; exist {
22772315
panic("http: multiple registrations for " + pattern)
22782316
}
22792317

22802318
if mux.m == nil {
22812319
mux.m = make(map[string]muxEntry)
22822320
}
2283-
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
2321+
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
22842322

22852323
if pattern[0] != '/' {
22862324
mux.hosts = true
22872325
}
2288-
2289-
// Helpful behavior:
2290-
// If pattern is /tree/, insert an implicit permanent redirect for /tree.
2291-
// It can be overridden by an explicit registration.
2292-
n := len(pattern)
2293-
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
2294-
// If pattern contains a host name, strip it and use remaining
2295-
// path for redirect.
2296-
path := pattern
2297-
if pattern[0] != '/' {
2298-
// In pattern, at least the last character is a '/', so
2299-
// strings.Index can't be -1.
2300-
path = pattern[strings.Index(pattern, "/"):]
2301-
}
2302-
url := &url.URL{Path: path}
2303-
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
2304-
}
23052326
}
23062327

23072328
// HandleFunc registers the handler function for the given pattern.

0 commit comments

Comments
 (0)