diff --git a/pkg/upstream/proxy.go b/pkg/upstream/proxy.go index 9b6246c8..3907eb0a 100644 --- a/pkg/upstream/proxy.go +++ b/pkg/upstream/proxy.go @@ -1,6 +1,7 @@ package upstream import ( + "context" "fmt" "net/http" "net/url" @@ -50,6 +51,8 @@ func NewProxy(upstreams options.Upstreams, sigData *options.SignatureData, write return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme) } } + + registerTrailingSlashHandler(m.serveMux) return m, nil } @@ -120,3 +123,25 @@ func (m *multiUpstreamProxy) registerRewriteHandler(upstream options.Upstream, h return nil } + +// registerTrailingSlashHandler creates a new matcher that will check if the +// requested path would match if it had a trailing slash appended. +// If the path matches with a trailing slash, we send back a redirect. +// This allows us to be consistent with the built in go servemux implementation. +func registerTrailingSlashHandler(serveMux *mux.Router) { + serveMux.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool { + if strings.HasSuffix(req.URL.Path, "/") { + return false + } + + // Use a separate RouteMatch so that we can redirect to the path + /. + // If we pass through the match then the matched backed will be served + // instead of the redirect handler. + m := &mux.RouteMatch{} + slashReq := req.Clone(context.Background()) + slashReq.URL.Path += "/" + return serveMux.Match(slashReq, m) + }).Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + http.Redirect(rw, req, req.URL.String()+"/", http.StatusMovedPermanently) + })) +} diff --git a/pkg/upstream/proxy_test.go b/pkg/upstream/proxy_test.go index 56570b14..5ac9842c 100644 --- a/pkg/upstream/proxy_test.go +++ b/pkg/upstream/proxy_test.go @@ -29,6 +29,7 @@ var _ = Describe("Proxy Suite", func() { } ok := http.StatusOK + accepted := http.StatusAccepted upstreams := options.Upstreams{ { @@ -47,6 +48,12 @@ var _ = Describe("Proxy Suite", func() { Static: true, StaticCode: &ok, }, + { + ID: "static-backend-no-trailing-slash", + Path: "/static", + Static: true, + StaticCode: &accepted, + }, { ID: "bad-http-backend", Path: "/bad-http/", @@ -235,5 +242,25 @@ var _ = Describe("Proxy Suite", func() { }, upstream: "backend-with-rewrite-prefix", }), + Entry("with a request to a path, missing the trailing slash", &proxyTableInput{ + target: "http://example.localhost/http", + response: testHTTPResponse{ + code: 301, + header: map[string][]string{ + contentType: {textHTMLUTF8}, + "Location": {"http://example.localhost/http/"}, + }, + raw: "Moved Permanently.\n\n", + }, + }), + Entry("with a request to a path, missing the trailing slash, but registered separately", &proxyTableInput{ + target: "http://example.localhost/static", + response: testHTTPResponse{ + code: 202, + header: map[string][]string{}, + raw: "Authenticated", + }, + upstream: "static-backend-no-trailing-slash", + }), ) }) diff --git a/pkg/upstream/upstream_suite_test.go b/pkg/upstream/upstream_suite_test.go index 5e9b9e32..e91da077 100644 --- a/pkg/upstream/upstream_suite_test.go +++ b/pkg/upstream/upstream_suite_test.go @@ -59,6 +59,7 @@ const ( acceptEncoding = "Accept-Encoding" applicationJSON = "application/json" textPlainUTF8 = "text/plain; charset=utf-8" + textHTMLUTF8 = "text/html; charset=utf-8" gapAuth = "Gap-Auth" gapSignature = "Gap-Signature" )