diff --git a/contrib/dimfeld/httptreemux.v5/httptreemux.go b/contrib/dimfeld/httptreemux.v5/httptreemux.go index 6169ac4b7e..512b94561a 100644 --- a/contrib/dimfeld/httptreemux.v5/httptreemux.go +++ b/contrib/dimfeld/httptreemux.v5/httptreemux.go @@ -112,18 +112,26 @@ func getRoute(router *httptreemux.TreeMux, w http.ResponseWriter, req *http.Requ if !found { return "", false } + routeLen := len(route) + trailingSlash := route[routeLen-1] == '/' && routeLen > 1 - // Check for redirecting route due to trailing slash for parameters. - // The redirecting behaviour originates from httptreemux router. - if lr.StatusCode == http.StatusMovedPermanently && strings.HasSuffix(route, "/") { + // Retry the population of lookup result parameters. + // If the initial attempt to populate the parameters fails, clone the request and modify the URI and URL Path. + // Depending on whether the route has a trailing slash or not, it will either add or remove the trailing slash and retry the lookup. + if routerRedirectEnabled(router) && isSupportedRedirectStatus(lr.StatusCode) && lr.Params == nil { rReq := req.Clone(req.Context()) - rReq.RequestURI = strings.TrimSuffix(rReq.RequestURI, "/") - rReq.URL.Path = strings.TrimSuffix(rReq.URL.Path, "/") - - lr, found = router.Lookup(w, rReq) - if !found { - return "", false + if trailingSlash { + // if the route has a trailing slash, remove it + rReq.RequestURI = strings.TrimSuffix(rReq.RequestURI, "/") + rReq.URL.Path = strings.TrimSuffix(rReq.URL.Path, "/") + } else { + // if the route does not have a trailing slash, add one + rReq.RequestURI = rReq.RequestURI + "/" + rReq.URL.Path = rReq.URL.Path + "/" } + // no need to check found again + // we already matched a route and we are only trying to populate the lookup result params + lr, _ = router.Lookup(w, rReq) } for k, v := range lr.Params { @@ -139,5 +147,31 @@ func getRoute(router *httptreemux.TreeMux, w http.ResponseWriter, req *http.Requ newP = "/:" + k route = strings.Replace(route, oldP, newP, 1) } + + // remove trailing slash from route to standardize returned value + // the router does not allow you to register two matching routes with the only difference being a trailing slash + // this only affects the resulting returned value and not the actual request URL set on tag http.url + if trailingSlash { + route = strings.TrimSuffix(route, "/") + } + return route, true } + +// isSupportedRedirectStatus checks if the given HTTP status code is a supported redirect status. +// It returns true if the status code is either StatusMovedPermanently, StatusTemporaryRedirect, or StatusPermanentRedirect. +// Otherwise, it returns false. +func isSupportedRedirectStatus(status int) bool { + return status == http.StatusMovedPermanently || + status == http.StatusTemporaryRedirect || + status == http.StatusPermanentRedirect +} + +// routerRedirectEnabled checks if the redirection is enabled on the router. +// It returns true if either RedirectCleanPath or RedirectTrailingSlash is enabled, +// and the RedirectBehavior is not set to UseHandler. Otherwise, it returns false. +// This function is used to determine whether to perform redirections based on the router's configuration. +func routerRedirectEnabled(router *httptreemux.TreeMux) bool { + return (router.RedirectCleanPath || router.RedirectTrailingSlash) && + router.RedirectBehavior != httptreemux.UseHandler +} diff --git a/contrib/dimfeld/httptreemux.v5/httptreemux_test.go b/contrib/dimfeld/httptreemux.v5/httptreemux_test.go index 017f2feedd..d9970917c9 100644 --- a/contrib/dimfeld/httptreemux.v5/httptreemux_test.go +++ b/contrib/dimfeld/httptreemux.v5/httptreemux_test.go @@ -238,18 +238,777 @@ func TestNamingSchema(t *testing.T) { namingschematest.NewHTTPServerTest(genSpans, "http.router")(t) } -func TestTrailingSlashRoutes(t *testing.T) { - t.Run("unknown if no handler matches", func(t *testing.T) { +func TestTrailingSlashRoutesWithBehaviorRedirect301(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { assert := assert.New(t) mt := mocktracer.Start() defer mt.Stop() - url := "/unknown/" - r := httptest.NewRequest(http.MethodGet, url, nil) + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.Redirect301 // default + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET unknown", s.Tag(ext.ResourceName)) + assert.Equal("404", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.NotContains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramvalue/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect301 // default + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusMovedPermanently, w.Code) + assert.Contains(w.Body.String(), "Moved Permanently") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestTrailingSlashRoutesWithBehaviorRedirect307(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.Redirect307 + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET unknown", s.Tag(ext.ResourceName)) + assert.Equal("404", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.NotContains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramvalue/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect307 + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusTemporaryRedirect, w.Code) + assert.Contains(w.Body.String(), "Temporary Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("307", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestTrailingSlashRoutesWithBehaviorRedirect308(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.Redirect308 + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET unknown", s.Tag(ext.ResourceName)) + assert.Equal("404", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.NotContains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramvalue/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.Redirect308 + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusPermanentRedirect, w.Code) + assert.Contains(w.Body.String(), "Permanent Redirect") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("308", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestTrailingSlashRoutesWithBehaviorUseHandler(t *testing.T) { + t.Run("GET unknown", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.RedirectBehavior = httptreemux.UseHandler + + // Note that the router has no handlers since we expect a 404 + + url := "/api/paramvalue" + r := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() - router().ServeHTTP(w, r) - assert.Equal(404, w.Code) - assert.Equal("404 page not found\n", w.Body.String()) + router.ServeHTTP(w, r) + + assert.Equal(http.StatusNotFound, w.Code) + assert.Contains(w.Body.String(), "404 page not found") spans := mt.FinishedSpans() assert.Equal(1, len(spans)) @@ -260,23 +1019,31 @@ func TestTrailingSlashRoutes(t *testing.T) { assert.Equal("GET unknown", s.Tag(ext.ResourceName)) assert.Equal("404", s.Tag(ext.HTTPCode)) assert.Equal("GET", s.Tag(ext.HTTPMethod)) - assert.Equal("http://example.com/unknown/", s.Tag(ext.HTTPURL)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) assert.Equal("testvalue", s.Tag("testkey")) assert.Nil(s.Tag(ext.Error)) assert.NotContains(s.Tags(), ext.HTTPRoute) }) - t.Run("parametrizes URL with trailing slash", func(t *testing.T) { + t.Run("GET /api/:parameter", func(t *testing.T) { assert := assert.New(t) mt := mocktracer.Start() defer mt.Stop() - url := "/api/paramvalue/" + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramvalue/" // with trailing slash r := httptest.NewRequest("GET", url, nil) w := httptest.NewRecorder() - router().ServeHTTP(w, r) - assert.Equal(301, w.Code) - assert.Contains(w.Body.String(), "Moved Permanently") + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") spans := mt.FinishedSpans() assert.Equal(1, len(spans)) @@ -284,14 +1051,384 @@ func TestTrailingSlashRoutes(t *testing.T) { s := spans[0] assert.Equal("http.request", s.OperationName()) assert.Equal("my-service", s.Tag(ext.ServiceName)) - assert.Equal("GET /api/:parameter/", s.Tag(ext.ResourceName)) - assert.Equal("301", s.Tag(ext.HTTPCode)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) assert.Equal("GET", s.Tag(ext.HTTPMethod)) assert.Equal("http://example.com/api/paramvalue/", s.Tag(ext.HTTPURL)) assert.Equal("testvalue", s.Tag("testkey")) assert.Nil(s.Tag(ext.Error)) assert.Contains(s.Tags(), ext.HTTPRoute) }) + + t.Run("GET /api/:parameter/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:parameter/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramvalue" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:parameter", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramvalue", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + router.GET("/api/:param1/:param2", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3/", handler200) // with trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2/paramval3" // without trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) + + t.Run("GET /api/:param1/:param2/:param3/", func(t *testing.T) { + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + router := New( + WithServiceName("my-service"), + WithSpanOptions(tracer.Tag("testkey", "testvalue")), + ) + + router.GET("/api/:param1/:param2/:param3", handler200) // without trailing slash + router.RedirectBehavior = httptreemux.UseHandler + + url := "/api/paramval1/paramval2/paramval3/" // with trailing slash + r := httptest.NewRequest("GET", url, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, r) + + assert.Equal(http.StatusOK, w.Code) + assert.Contains(w.Body.String(), "OK\n") + + spans := mt.FinishedSpans() + assert.Equal(1, len(spans)) + + s := spans[0] + assert.Equal("http.request", s.OperationName()) + assert.Equal("my-service", s.Tag(ext.ServiceName)) + assert.Equal("GET /api/:param1/:param2/:param3", s.Tag(ext.ResourceName)) + assert.Equal("200", s.Tag(ext.HTTPCode)) + assert.Equal("GET", s.Tag(ext.HTTPMethod)) + assert.Equal("http://example.com/api/paramval1/paramval2/paramval3/", s.Tag(ext.HTTPURL)) + assert.Equal("testvalue", s.Tag("testkey")) + assert.Nil(s.Tag(ext.Error)) + assert.Contains(s.Tags(), ext.HTTPRoute) + }) +} + +func TestIsSupportedRedirectStatus(t *testing.T) { + tests := []struct { + name string + status int + want bool + }{ + { + name: "Test with status 301", + status: 301, + want: true, + }, + { + name: "Test with status 302", + status: 302, + want: false, + }, + { + name: "Test with status 303", + status: 303, + want: false, + }, + { + name: "Test with status 307", + status: 307, + want: true, + }, + { + name: "Test with status 308", + status: 308, + want: true, + }, + { + name: "Test with status 400", + status: 400, + want: false, + }, + { + name: "Test with status 0", + status: 0, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isSupportedRedirectStatus(tt.status); got != tt.want { + t.Errorf("isSupportedRedirectStatus() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRouterRedirectEnabled(t *testing.T) { + tests := []struct { + name string + cleanPath bool + trailingSlash bool + redirectBehaviour httptreemux.RedirectBehavior + + want bool + }{ + // httptreemux.Redirect301 + { + name: "Test Redirect301 with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect301, + want: true, + }, + { + name: "Test Redirect301 with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect301, + want: true, + }, + { + name: "Test Redirect301 with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect301, + want: true, + }, + { + name: "Test Redirect301 with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect301, + want: false, + }, + // httptreemux.Redirect307 + { + name: "Test Redirect307 with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect307, + want: true, + }, + { + name: "Test Redirect307 with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect307, + want: true, + }, + { + name: "Test Redirect307 with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect307, + want: true, + }, + { + name: "Test Redirect307 with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect307, + want: false, + }, + // httptreemux.Redirect308 + { + name: "Test Redirect308 with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect308, + want: true, + }, + { + name: "Test Redirect308 with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect308, + want: true, + }, + { + name: "Test Redirect308 with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.Redirect308, + want: true, + }, + { + name: "Test Redirect308 with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.Redirect308, + want: false, + }, + // httptreemux.UseHandler + { + name: "Test UseHandler with clean path and trailing slash", + cleanPath: true, + trailingSlash: true, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + { + name: "Test UseHandler with clean path and no trailing slash", + cleanPath: true, + trailingSlash: false, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + { + name: "Test UseHandler with no clean path and trailing slash", + cleanPath: false, + trailingSlash: true, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + { + name: "Test UseHandler with no clean path and no trailing slash", + cleanPath: false, + trailingSlash: false, + redirectBehaviour: httptreemux.UseHandler, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router := httptreemux.New() + router.RedirectCleanPath = tt.cleanPath + router.RedirectTrailingSlash = tt.trailingSlash + router.RedirectBehavior = tt.redirectBehaviour + + if got := routerRedirectEnabled(router); got != tt.want { + t.Errorf("routerRedirectEnabled() = %v, want %v", got, tt.want) + } + }) + } } func router() http.Handler { @@ -302,7 +1439,9 @@ func router() http.Handler { router.GET("/200", handler200) router.GET("/500", handler500) - router.GET("/api/:parameter", handlerDummy) + + router.GET("/api/:parameter", handler200) + router.GET("/api/:param1/:param2/:param3", handler200) return router } @@ -314,6 +1453,3 @@ func handler200(w http.ResponseWriter, _ *http.Request, _ map[string]string) { func handler500(w http.ResponseWriter, _ *http.Request, _ map[string]string) { http.Error(w, "500!", http.StatusInternalServerError) } -func handlerDummy(w http.ResponseWriter, _ *http.Request, _ map[string]string) { - w.WriteHeader(http.StatusAccepted) -}