Repository: labstack/echo (go)

Issue:
Wrong documentation on Timeout middleware
### Issue Description
The [Timeout middleware godoc](https://github.com/labstack/echo/blob/dec96f0312ee1e1117366a420c57dd744444d0da/middleware/timeout.go#L45-L46) is wrong.

### Checklist

- [x] Dependencies installed
- [x] No typos
- [x] Searched existing issues and docs

### Expected behaviour
The godocs for the `Timeout` handler should describe what it does.

### Actual behaviour
The godocs for the `Timeout` handler describe what the `Recover` middleware does.

### Steps to reproduce

### Working code to debug

```bash
$ go doc github.com/labstack/echo/v4/middleware.Timeout
package middleware // import "github.com/labstack/echo/v4/middleware"

func Timeout() echo.MiddlewareFunc
    Timeout returns a middleware which recovers from panics anywhere in the
    chain and handles the control to the centralized HTTPErrorHandler.

```

### Version/commit
All commits since 67263b5e456480224b2e4bb1e96b75d6a8ef54a5, the first commit where the `Timeout` middleware function was introduced.

Pull request:
fix timeout middleware docs
fix timeout middleware docs (fixes #1816)

Patch:
diff --git a/bind.go b/bind.go
index 16c3b7a..08d3989 100644
--- a/bind.go
+++ b/bind.go
@@ -134,6 +134,10 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
 
 	// !struct
 	if typ.Kind() != reflect.Struct {
+		if tag == "param" || tag == "query" {
+			// incompatible type, data is probably to be found in the body
+			return nil
+		}
 		return errors.New("binding element must be a struct")
 	}
 
diff --git a/middleware/redirect.go b/middleware/redirect.go
index 813e5b8..13877db 100644
--- a/middleware/redirect.go
+++ b/middleware/redirect.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"net/http"
+	"strings"
 
 	"github.com/labstack/echo/v4"
 )
@@ -40,11 +41,11 @@ func HTTPSRedirect() echo.MiddlewareFunc {
 // HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
 // See `HTTPSRedirect()`.
 func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
-	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = scheme != "https"; ok {
-			url = "https://" + host + uri
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if scheme != "https" {
+			return true, "https://" + host + uri
 		}
-		return
+		return false, ""
 	})
 }
 
@@ -59,11 +60,11 @@ func HTTPSWWWRedirect() echo.MiddlewareFunc {
 // HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
 // See `HTTPSWWWRedirect()`.
 func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
-	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = scheme != "https" && host[:4] != www; ok {
-			url = "https://www." + host + uri
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if scheme != "https" && !strings.HasPrefix(host, www) {
+			return true, "https://www." + host + uri
 		}
-		return
+		return false, ""
 	})
 }
 
@@ -79,13 +80,11 @@ func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
 // See `HTTPSNonWWWRedirect()`.
 func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
 	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = scheme != "https"; ok {
-			if host[:4] == www {
-				host = host[4:]
-			}
-			url = "https://" + host + uri
+		if scheme != "https" {
+			host = strings.TrimPrefix(host, www)
+			return true, "https://" + host + uri
 		}
-		return
+		return false, ""
 	})
 }
 
@@ -100,11 +99,11 @@ func WWWRedirect() echo.MiddlewareFunc {
 // WWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
 // See `WWWRedirect()`.
 func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
-	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = host[:4] != www; ok {
-			url = scheme + "://www." + host + uri
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if !strings.HasPrefix(host, www) {
+			return true, scheme + "://www." + host + uri
 		}
-		return
+		return false, ""
 	})
 }
 
@@ -119,17 +118,17 @@ func NonWWWRedirect() echo.MiddlewareFunc {
 // NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
 // See `NonWWWRedirect()`.
 func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
-	return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
-		if ok = host[:4] == www; ok {
-			url = scheme + "://" + host[4:] + uri
+	return redirect(config, func(scheme, host, uri string) (bool, string) {
+		if strings.HasPrefix(host, www) {
+			return true, scheme + "://" + host[4:] + uri
 		}
-		return
+		return false, ""
 	})
 }
 
 func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc {
 	if config.Skipper == nil {
-		config.Skipper = DefaultTrailingSlashConfig.Skipper
+		config.Skipper = DefaultRedirectConfig.Skipper
 	}
 	if config.Code == 0 {
 		config.Code = DefaultRedirectConfig.Code
diff --git a/middleware/timeout.go b/middleware/timeout.go
index 99d436a..5d23ff4 100644
--- a/middleware/timeout.go
+++ b/middleware/timeout.go
@@ -42,8 +42,8 @@ var (
 	}
 )
 
-// Timeout returns a middleware which recovers from panics anywhere in the chain
-// and handles the control to the centralized HTTPErrorHandler.
+// Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler
+// call runs for longer than its time limit. NB: timeout does not stop handler execution.
 func Timeout() echo.MiddlewareFunc {
 	return TimeoutWithConfig(DefaultTimeoutConfig)
 }


Test patch:
diff --git a/bind_test.go b/bind_test.go
index e8868b3..7339803 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -9,6 +9,7 @@ import (
 	"mime/multipart"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"reflect"
 	"strconv"
 	"strings"
@@ -187,7 +188,10 @@ func TestToMultipleFields(t *testing.T) {
 
 func TestBindJSON(t *testing.T) {
 	assert := assert.New(t)
-	testBindOkay(assert, strings.NewReader(userJSON), MIMEApplicationJSON)
+	testBindOkay(assert, strings.NewReader(userJSON), nil, MIMEApplicationJSON)
+	testBindOkay(assert, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON)
+	testBindArrayOkay(assert, strings.NewReader(usersJSON), nil, MIMEApplicationJSON)
+	testBindArrayOkay(assert, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON)
 	testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
 	testBindError(assert, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{})
 }
@@ -195,11 +199,15 @@ func TestBindJSON(t *testing.T) {
 func TestBindXML(t *testing.T) {
 	assert := assert.New(t)
 
-	testBindOkay(assert, strings.NewReader(userXML), MIMEApplicationXML)
+	testBindOkay(assert, strings.NewReader(userXML), nil, MIMEApplicationXML)
+	testBindOkay(assert, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
+	testBindArrayOkay(assert, strings.NewReader(userXML), nil, MIMEApplicationXML)
+	testBindArrayOkay(assert, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
 	testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New(""))
 	testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{})
 	testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{})
-	testBindOkay(assert, strings.NewReader(userXML), MIMETextXML)
+	testBindOkay(assert, strings.NewReader(userXML), nil, MIMETextXML)
+	testBindOkay(assert, strings.NewReader(userXML), dummyQuery, MIMETextXML)
 	testBindError(assert, strings.NewReader(invalidContent), MIMETextXML, errors.New(""))
 	testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{})
 	testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{})
@@ -208,7 +216,8 @@ func TestBindXML(t *testing.T) {
 func TestBindForm(t *testing.T) {
 	assert := assert.New(t)
 
-	testBindOkay(assert, strings.NewReader(userForm), MIMEApplicationForm)
+	testBindOkay(assert, strings.NewReader(userForm), nil, MIMEApplicationForm)
+	testBindOkay(assert, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm)
 	e := New()
 	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userForm))
 	rec := httptest.NewRecorder()
@@ -336,14 +345,16 @@ func TestBindUnmarshalTextPtr(t *testing.T) {
 }
 
 func TestBindMultipartForm(t *testing.T) {
-	body := new(bytes.Buffer)
-	mw := multipart.NewWriter(body)
+	bodyBuffer := new(bytes.Buffer)
+	mw := multipart.NewWriter(bodyBuffer)
 	mw.WriteField("id", "1")
 	mw.WriteField("name", "Jon Snow")
 	mw.Close()
+	body := bodyBuffer.Bytes()
 
 	assert := assert.New(t)
-	testBindOkay(assert, body, mw.FormDataContentType())
+	testBindOkay(assert, bytes.NewReader(body), nil, mw.FormDataContentType())
+	testBindOkay(assert, bytes.NewReader(body), dummyQuery, mw.FormDataContentType())
 }
 
 func TestBindUnsupportedMediaType(t *testing.T) {
@@ -547,9 +558,13 @@ func assertBindTestStruct(a *assert.Assertions, ts *bindTestStruct) {
 	a.Equal("", ts.GetCantSet())
 }
 
-func testBindOkay(assert *assert.Assertions, r io.Reader, ctype string) {
+func testBindOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctype string) {
 	e := New()
-	req := httptest.NewRequest(http.MethodPost, "/", r)
+	path := "/"
+	if len(query) > 0 {
+		path += "?" + query.Encode()
+	}
+	req := httptest.NewRequest(http.MethodPost, path, r)
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	req.Header.Set(HeaderContentType, ctype)
@@ -561,6 +576,25 @@ func testBindOkay(assert *assert.Assertions, r io.Reader, ctype string) {
 	}
 }
 
+func testBindArrayOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctype string) {
+	e := New()
+	path := "/"
+	if len(query) > 0 {
+		path += "?" + query.Encode()
+	}
+	req := httptest.NewRequest(http.MethodPost, path, r)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	req.Header.Set(HeaderContentType, ctype)
+	u := []user{}
+	err := c.Bind(&u)
+	if assert.NoError(err) {
+		assert.Equal(1, len(u))
+		assert.Equal(1, u[0].ID)
+		assert.Equal("Jon Snow", u[0].Name)
+	}
+}
+
 func testBindError(assert *assert.Assertions, r io.Reader, ctype string, expectedInternal error) {
 	e := New()
 	req := httptest.NewRequest(http.MethodPost, "/", r)
@@ -679,15 +713,16 @@ func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
 			expect:       &Opts{ID: 0, Node: "xxx"}, // query binding has already modified bind target
 			expectError:  "code=400, message=Unmarshal type error: expected=echo.Opts, got=array, field=, offset=1, internal=json: cannot unmarshal array into Go value of type echo.Opts",
 		},
-		{ // binding query params interferes with body. b.BindBody() should be used to bind only body to slice
-			name:             "nok, GET query params bind failure - trying to bind json array to slice",
+		{ // query param is ignored as we do not know where exactly to bind it in slice
+			name:             "ok, GET bind to struct slice, ignore query param",
 			givenMethod:      http.MethodGet,
 			givenURL:         "/api/real_node/endpoint?node=xxx",
 			givenContent:     strings.NewReader(`[{"id": 1}]`),
 			whenNoPathParams: true,
 			whenBindTarget:   &[]Opts{},
-			expect:           &[]Opts{},
-			expectError:      "code=400, message=binding element must be a struct, internal=binding element must be a struct",
+			expect: &[]Opts{
+				{ID: 1, Node: ""},
+			},
 		},
 		{ // binding query params interferes with body. b.BindBody() should be used to bind only body to slice
 			name:             "ok, POST binding to slice should not be affected query params types",
@@ -699,14 +734,15 @@ func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
 			expect:           &[]Opts{{ID: 1}},
 			expectError:      "",
 		},
-		{ // binding path params interferes with body. b.BindBody() should be used to bind only body to slice
-			name:           "nok, GET path params bind failure - trying to bind json array to slice",
+		{ // path param is ignored as we do not know where exactly to bind it in slice
+			name:           "ok, GET bind to struct slice, ignore path param",
 			givenMethod:    http.MethodGet,
 			givenURL:       "/api/real_node/endpoint?node=xxx",
 			givenContent:   strings.NewReader(`[{"id": 1}]`),
 			whenBindTarget: &[]Opts{},
-			expect:         &[]Opts{},
-			expectError:    "code=400, message=binding element must be a struct, internal=binding element must be a struct",
+			expect: &[]Opts{
+				{ID: 1, Node: ""},
+			},
 		},
 		{
 			name:             "ok, GET body bind json array to slice",
diff --git a/echo_test.go b/echo_test.go
index 35c79cb..58ecea7 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -10,6 +10,7 @@ import (
 	"net"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"os"
 	"reflect"
 	"strings"
@@ -30,6 +31,7 @@ type (
 
 const (
 	userJSON                    = `{"id":1,"name":"Jon Snow"}`
+	usersJSON                   = `[{"id":1,"name":"Jon Snow"}]`
 	userXML                     = `<user><id>1</id><name>Jon Snow</name></user>`
 	userForm                    = `id=1&name=Jon Snow`
 	invalidContent              = "invalid content"
@@ -48,6 +50,8 @@ const userXMLPretty = `<user>
   <name>Jon Snow</name>
 </user>`
 
+var dummyQuery = url.Values{"dummy": []string{"useless"}}
+
 func TestEcho(t *testing.T) {
 	e := New()
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
diff --git a/middleware/redirect_test.go b/middleware/redirect_test.go
index 0826095..9d1b562 100644
--- a/middleware/redirect_test.go
+++ b/middleware/redirect_test.go
@@ -12,62 +12,253 @@ import (
 type middlewareGenerator func() echo.MiddlewareFunc
 
 func TestRedirectHTTPSRedirect(t *testing.T) {
-	res := redirectTest(HTTPSRedirect, "labstack.com", nil)
-
-	assert.Equal(t, http.StatusMovedPermanently, res.Code)
-	assert.Equal(t, "https://labstack.com/", res.Header().Get(echo.HeaderLocation))
-}
+	var testCases = []struct {
+		whenHost         string
+		whenHeader       http.Header
+		expectLocation   string
+		expectStatusCode int
+	}{
+		{
+			whenHost:         "labstack.com",
+			expectLocation:   "https://labstack.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "labstack.com",
+			whenHeader:       map[string][]string{echo.HeaderXForwardedProto: {"https"}},
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+	}
 
-func TestHTTPSRedirectBehindTLSTerminationProxy(t *testing.T) {
-	header := http.Header{}
-	header.Set(echo.HeaderXForwardedProto, "https")
-	res := redirectTest(HTTPSRedirect, "labstack.com", header)
+	for _, tc := range testCases {
+		t.Run(tc.whenHost, func(t *testing.T) {
+			res := redirectTest(HTTPSRedirect, tc.whenHost, tc.whenHeader)
 
-	assert.Equal(t, http.StatusOK, res.Code)
+			assert.Equal(t, tc.expectStatusCode, res.Code)
+			assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))
+		})
+	}
 }
 
 func TestRedirectHTTPSWWWRedirect(t *testing.T) {
-	res := redirectTest(HTTPSWWWRedirect, "labstack.com", nil)
-
-	assert.Equal(t, http.StatusMovedPermanently, res.Code)
-	assert.Equal(t, "https://www.labstack.com/", res.Header().Get(echo.HeaderLocation))
-}
+	var testCases = []struct {
+		whenHost         string
+		whenHeader       http.Header
+		expectLocation   string
+		expectStatusCode int
+	}{
+		{
+			whenHost:         "labstack.com",
+			expectLocation:   "https://www.labstack.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "www.labstack.com",
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+		{
+			whenHost:         "a.com",
+			expectLocation:   "https://www.a.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "ip",
+			expectLocation:   "https://www.ip/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "labstack.com",
+			whenHeader:       map[string][]string{echo.HeaderXForwardedProto: {"https"}},
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+	}
 
-func TestRedirectHTTPSWWWRedirectBehindTLSTerminationProxy(t *testing.T) {
-	header := http.Header{}
-	header.Set(echo.HeaderXForwardedProto, "https")
-	res := redirectTest(HTTPSWWWRedirect, "labstack.com", header)
+	for _, tc := range testCases {
+		t.Run(tc.whenHost, func(t *testing.T) {
+			res := redirectTest(HTTPSWWWRedirect, tc.whenHost, tc.whenHeader)
 
-	assert.Equal(t, http.StatusOK, res.Code)
+			assert.Equal(t, tc.expectStatusCode, res.Code)
+			assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))
+		})
+	}
 }
 
 func TestRedirectHTTPSNonWWWRedirect(t *testing.T) {
-	res := redirectTest(HTTPSNonWWWRedirect, "www.labstack.com", nil)
-
-	assert.Equal(t, http.StatusMovedPermanently, res.Code)
-	assert.Equal(t, "https://labstack.com/", res.Header().Get(echo.HeaderLocation))
-}
+	var testCases = []struct {
+		whenHost         string
+		whenHeader       http.Header
+		expectLocation   string
+		expectStatusCode int
+	}{
+		{
+			whenHost:         "www.labstack.com",
+			expectLocation:   "https://labstack.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "a.com",
+			expectLocation:   "https://a.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "ip",
+			expectLocation:   "https://ip/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "www.labstack.com",
+			whenHeader:       map[string][]string{echo.HeaderXForwardedProto: {"https"}},
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+	}
 
-func TestRedirectHTTPSNonWWWRedirectBehindTLSTerminationProxy(t *testing.T) {
-	header := http.Header{}
-	header.Set(echo.HeaderXForwardedProto, "https")
-	res := redirectTest(HTTPSNonWWWRedirect, "www.labstack.com", header)
+	for _, tc := range testCases {
+		t.Run(tc.whenHost, func(t *testing.T) {
+			res := redirectTest(HTTPSNonWWWRedirect, tc.whenHost, tc.whenHeader)
 
-	assert.Equal(t, http.StatusOK, res.Code)
+			assert.Equal(t, tc.expectStatusCode, res.Code)
+			assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))
+		})
+	}
 }
 
 func TestRedirectWWWRedirect(t *testing.T) {
-	res := redirectTest(WWWRedirect, "labstack.com", nil)
+	var testCases = []struct {
+		whenHost         string
+		whenHeader       http.Header
+		expectLocation   string
+		expectStatusCode int
+	}{
+		{
+			whenHost:         "labstack.com",
+			expectLocation:   "http://www.labstack.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "a.com",
+			expectLocation:   "http://www.a.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "ip",
+			expectLocation:   "http://www.ip/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "a.com",
+			whenHeader:       map[string][]string{echo.HeaderXForwardedProto: {"https"}},
+			expectLocation:   "https://www.a.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "www.ip",
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.whenHost, func(t *testing.T) {
+			res := redirectTest(WWWRedirect, tc.whenHost, tc.whenHeader)
 
-	assert.Equal(t, http.StatusMovedPermanently, res.Code)
-	assert.Equal(t, "http://www.labstack.com/", res.Header().Get(echo.HeaderLocation))
+			assert.Equal(t, tc.expectStatusCode, res.Code)
+			assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))
+		})
+	}
 }
 
 func TestRedirectNonWWWRedirect(t *testing.T) {
-	res := redirectTest(NonWWWRedirect, "www.labstack.com", nil)
+	var testCases = []struct {
+		whenHost         string
+		whenHeader       http.Header
+		expectLocation   string
+		expectStatusCode int
+	}{
+		{
+			whenHost:         "www.labstack.com",
+			expectLocation:   "http://labstack.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "www.a.com",
+			expectLocation:   "http://a.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "www.a.com",
+			whenHeader:       map[string][]string{echo.HeaderXForwardedProto: {"https"}},
+			expectLocation:   "https://a.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			whenHost:         "ip",
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.whenHost, func(t *testing.T) {
+			res := redirectTest(NonWWWRedirect, tc.whenHost, tc.whenHeader)
+
+			assert.Equal(t, tc.expectStatusCode, res.Code)
+			assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))
+		})
+	}
+}
+
+func TestNonWWWRedirectWithConfig(t *testing.T) {
+	var testCases = []struct {
+		name             string
+		givenCode        int
+		givenSkipFunc    func(c echo.Context) bool
+		whenHost         string
+		whenHeader       http.Header
+		expectLocation   string
+		expectStatusCode int
+	}{
+		{
+			name:             "usual redirect",
+			whenHost:         "www.labstack.com",
+			expectLocation:   "http://labstack.com/",
+			expectStatusCode: http.StatusMovedPermanently,
+		},
+		{
+			name: "redirect is skipped",
+			givenSkipFunc: func(c echo.Context) bool {
+				return true // skip always
+			},
+			whenHost:         "www.labstack.com",
+			expectLocation:   "",
+			expectStatusCode: http.StatusOK,
+		},
+		{
+			name:             "redirect with custom status code",
+			givenCode:        http.StatusSeeOther,
+			whenHost:         "www.labstack.com",
+			expectLocation:   "http://labstack.com/",
+			expectStatusCode: http.StatusSeeOther,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.whenHost, func(t *testing.T) {
+			middleware := func() echo.MiddlewareFunc {
+				return NonWWWRedirectWithConfig(RedirectConfig{
+					Skipper: tc.givenSkipFunc,
+					Code:    tc.givenCode,
+				})
+			}
+			res := redirectTest(middleware, tc.whenHost, tc.whenHeader)
 
-	assert.Equal(t, http.StatusMovedPermanently, res.Code)
-	assert.Equal(t, "http://labstack.com/", res.Header().Get(echo.HeaderLocation))
+			assert.Equal(t, tc.expectStatusCode, res.Code)
+			assert.Equal(t, tc.expectLocation, res.Header().Get(echo.HeaderLocation))
+		})
+	}
 }
 
 func redirectTest(fn middlewareGenerator, host string, header http.Header) *httptest.ResponseRecorder {