Repository: labstack/echo
Pull request #1864 (2021-04-28 15:29:35)

Fix wrong status code returning in case of handler with middleware.Timeout throws the panic
I found out that when the `echo.HandlerFunc` with `middleware.Timeout` calls `panic`, global `middleware.Recover()` could return a wrong status code to the client, cause inside `echoHandlerFuncWrapper.ServeHTTP` method the `originalWriter` wasn't restored if handler throws the panic.
This pull request must fix this issue.


Patch:
diff --git a/middleware/jwt.go b/middleware/jwt.go
index da00ea5..cd35b62 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -29,15 +29,19 @@ type (
 		// ErrorHandlerWithContext is almost identical to ErrorHandler, but it's passed the current context.
 		ErrorHandlerWithContext JWTErrorHandlerWithContext
 
-		// Signing key to validate token. Used as fallback if SigningKeys has length 0.
-		// Required. This or SigningKeys.
+		// Signing key to validate token.
+		// This is one of the three options to provide a token validation key.
+		// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
+		// Required if neither user-defined KeyFunc nor SigningKeys is provided.
 		SigningKey interface{}
 
 		// Map of signing keys to validate token with kid field usage.
-		// Required. This or SigningKey.
+		// This is one of the three options to provide a token validation key.
+		// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
+		// Required if neither user-defined KeyFunc nor SigningKey is provided.
 		SigningKeys map[string]interface{}
 
-		// Signing method, used to check token signing method.
+		// Signing method used to check the token's signing algorithm.
 		// Optional. Default value HS256.
 		SigningMethod string
 
@@ -64,7 +68,16 @@ type (
 		// Optional. Default value "Bearer".
 		AuthScheme string
 
-		keyFunc jwt.Keyfunc
+		// KeyFunc defines a user-defined function that supplies the public key for a token validation.
+		// The function shall take care of verifying the signing algorithm and selecting the proper key.
+		// A user-defined KeyFunc can be useful if tokens are issued by an external party.
+		//
+		// When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.
+		// This is one of the three options to provide a token validation key.
+		// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
+		// Required if neither SigningKeys nor SigningKey is provided.
+		// Default to an internal implementation verifying the signing algorithm and selecting the proper key.
+		KeyFunc jwt.Keyfunc
 	}
 
 	// JWTSuccessHandler defines a function which is executed for a valid token.
@@ -99,6 +112,7 @@ var (
 		TokenLookup:   "header:" + echo.HeaderAuthorization,
 		AuthScheme:    "Bearer",
 		Claims:        jwt.MapClaims{},
+		KeyFunc:       nil,
 	}
 )
 
@@ -123,7 +137,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 	if config.Skipper == nil {
 		config.Skipper = DefaultJWTConfig.Skipper
 	}
-	if config.SigningKey == nil && len(config.SigningKeys) == 0 {
+	if config.SigningKey == nil && len(config.SigningKeys) == 0 && config.KeyFunc == nil {
 		panic("echo: jwt middleware requires signing key")
 	}
 	if config.SigningMethod == "" {
@@ -141,35 +155,29 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 	if config.AuthScheme == "" {
 		config.AuthScheme = DefaultJWTConfig.AuthScheme
 	}
-	config.keyFunc = func(t *jwt.Token) (interface{}, error) {
-		// Check the signing method
-		if t.Method.Alg() != config.SigningMethod {
-			return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
-		}
-		if len(config.SigningKeys) > 0 {
-			if kid, ok := t.Header["kid"].(string); ok {
-				if key, ok := config.SigningKeys[kid]; ok {
-					return key, nil
-				}
-			}
-			return nil, fmt.Errorf("unexpected jwt key id=%v", t.Header["kid"])
-		}
-
-		return config.SigningKey, nil
+	if config.KeyFunc == nil {
+		config.KeyFunc = config.defaultKeyFunc
 	}
 
 	// Initialize
-	parts := strings.Split(config.TokenLookup, ":")
-	extractor := jwtFromHeader(parts[1], config.AuthScheme)
-	switch parts[0] {
-	case "query":
-		extractor = jwtFromQuery(parts[1])
-	case "param":
-		extractor = jwtFromParam(parts[1])
-	case "cookie":
-		extractor = jwtFromCookie(parts[1])
-	case "form":
-		extractor = jwtFromForm(parts[1])
+	// Split sources
+	sources := strings.Split(config.TokenLookup, ",")
+	var extractors []jwtExtractor
+	for _, source := range sources {
+		parts := strings.Split(source, ":")
+
+		switch parts[0] {
+		case "query":
+			extractors = append(extractors, jwtFromQuery(parts[1]))
+		case "param":
+			extractors = append(extractors, jwtFromParam(parts[1]))
+		case "cookie":
+			extractors = append(extractors, jwtFromCookie(parts[1]))
+		case "form":
+			extractors = append(extractors, jwtFromForm(parts[1]))
+		case "header":
+			extractors = append(extractors, jwtFromHeader(parts[1], config.AuthScheme))
+		}
 	}
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -181,8 +189,17 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 			if config.BeforeFunc != nil {
 				config.BeforeFunc(c)
 			}
-
-			auth, err := extractor(c)
+			var auth string
+			var err error
+			for _, extractor := range extractors {
+				// Extract token from extractor, if it's not fail break the loop and
+				// set auth
+				auth, err = extractor(c)
+				if err == nil {
+					break
+				}
+			}
+			// If none of extractor has a token, handle error
 			if err != nil {
 				if config.ErrorHandler != nil {
 					return config.ErrorHandler(err)
@@ -193,14 +210,15 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 				}
 				return err
 			}
+
 			token := new(jwt.Token)
 			// Issue #647, #656
 			if _, ok := config.Claims.(jwt.MapClaims); ok {
-				token, err = jwt.Parse(auth, config.keyFunc)
+				token, err = jwt.Parse(auth, config.KeyFunc)
 			} else {
 				t := reflect.ValueOf(config.Claims).Type().Elem()
 				claims := reflect.New(t).Interface().(jwt.Claims)
-				token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc)
+				token, err = jwt.ParseWithClaims(auth, claims, config.KeyFunc)
 			}
 			if err == nil && token.Valid {
 				// Store user information from token into context.
@@ -225,6 +243,24 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 	}
 }
 
+// defaultKeyFunc returns a signing key of the given token.
+func (config *JWTConfig) defaultKeyFunc(t *jwt.Token) (interface{}, error) {
+	// Check the signing method
+	if t.Method.Alg() != config.SigningMethod {
+		return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
+	}
+	if len(config.SigningKeys) > 0 {
+		if kid, ok := t.Header["kid"].(string); ok {
+			if key, ok := config.SigningKeys[kid]; ok {
+				return key, nil
+			}
+		}
+		return nil, fmt.Errorf("unexpected jwt key id=%v", t.Header["kid"])
+	}
+
+	return config.SigningKey, nil
+}
+
 // jwtFromHeader returns a `jwtExtractor` that extracts token from the request header.
 func jwtFromHeader(header string, authScheme string) jwtExtractor {
 	return func(c echo.Context) (string, error) {
diff --git a/middleware/key_auth.go b/middleware/key_auth.go
index 94cfd14..fd169aa 100644
--- a/middleware/key_auth.go
+++ b/middleware/key_auth.go
@@ -30,12 +30,19 @@ type (
 		// Validator is a function to validate key.
 		// Required.
 		Validator KeyAuthValidator
+
+		// ErrorHandler defines a function which is executed for an invalid key.
+		// It may be used to define a custom error.
+		ErrorHandler KeyAuthErrorHandler
 	}
 
 	// KeyAuthValidator defines a function to validate KeyAuth credentials.
 	KeyAuthValidator func(string, echo.Context) (bool, error)
 
 	keyExtractor func(echo.Context) (string, error)
+
+	// KeyAuthErrorHandler defines a function which is executed for an invalid key.
+	KeyAuthErrorHandler func(error, echo.Context) error
 )
 
 var (
@@ -95,10 +102,16 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
 			// Extract and verify key
 			key, err := extractor(c)
 			if err != nil {
+				if config.ErrorHandler != nil {
+					return config.ErrorHandler(err, c)
+				}
 				return echo.NewHTTPError(http.StatusBadRequest, err.Error())
 			}
 			valid, err := config.Validator(key, c)
 			if err != nil {
+				if config.ErrorHandler != nil {
+					return config.ErrorHandler(err, c)
+				}
 				return &echo.HTTPError{
 					Code:     http.StatusUnauthorized,
 					Message:  "invalid key",
diff --git a/middleware/static.go b/middleware/static.go
index ae79cb5..0106f7c 100644
--- a/middleware/static.go
+++ b/middleware/static.go
@@ -42,6 +42,10 @@ type (
 		// the filesystem path is not doubled
 		// Optional. Default value false.
 		IgnoreBase bool `yaml:"ignoreBase"`
+
+		// Filesystem provides access to the static content.
+		// Optional. Defaults to http.Dir(config.Root)
+		Filesystem http.FileSystem `yaml:"-"`
 	}
 )
 
@@ -146,6 +150,10 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
 	if config.Index == "" {
 		config.Index = DefaultStaticConfig.Index
 	}
+	if config.Filesystem == nil {
+		config.Filesystem = http.Dir(config.Root)
+		config.Root = "."
+	}
 
 	// Index template
 	t, err := template.New("index").Parse(html)
@@ -178,49 +186,73 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
 				}
 			}
 
-			fi, err := os.Stat(name)
+			file, err := openFile(config.Filesystem, name)
 			if err != nil {
-				if os.IsNotExist(err) {
-					if err = next(c); err != nil {
-						if he, ok := err.(*echo.HTTPError); ok {
-							if config.HTML5 && he.Code == http.StatusNotFound {
-								return c.File(filepath.Join(config.Root, config.Index))
-							}
-						}
-						return
-					}
+				if !os.IsNotExist(err) {
+					return err
+				}
+
+				if err = next(c); err == nil {
+					return err
+				}
+
+				he, ok := err.(*echo.HTTPError)
+				if !(ok && config.HTML5 && he.Code == http.StatusNotFound) {
+					return err
+				}
+
+				file, err = openFile(config.Filesystem, filepath.Join(config.Root, config.Index))
+				if err != nil {
+					return err
 				}
-				return
 			}
 
-			if fi.IsDir() {
-				index := filepath.Join(name, config.Index)
-				fi, err = os.Stat(index)
+			defer file.Close()
+
+			info, err := file.Stat()
+			if err != nil {
+				return err
+			}
 
+			if info.IsDir() {
+				index, err := openFile(config.Filesystem, filepath.Join(name, config.Index))
 				if err != nil {
 					if config.Browse {
-						return listDir(t, name, c.Response())
+						return listDir(t, name, file, c.Response())
 					}
+
 					if os.IsNotExist(err) {
 						return next(c)
 					}
-					return
 				}
 
-				return c.File(index)
+				defer index.Close()
+
+				info, err = index.Stat()
+				if err != nil {
+					return err
+				}
+
+				return serveFile(c, index, info)
 			}
 
-			return c.File(name)
+			return serveFile(c, file, info)
 		}
 	}
 }
 
-func listDir(t *template.Template, name string, res *echo.Response) (err error) {
-	file, err := os.Open(name)
-	if err != nil {
-		return
-	}
-	files, err := file.Readdir(-1)
+func openFile(fs http.FileSystem, name string) (http.File, error) {
+	pathWithSlashes := filepath.ToSlash(name)
+	return fs.Open(pathWithSlashes)
+}
+
+func serveFile(c echo.Context, file http.File, info os.FileInfo) error {
+	http.ServeContent(c.Response(), c.Request(), info.Name(), info.ModTime(), file)
+	return nil
+}
+
+func listDir(t *template.Template, name string, dir http.File, res *echo.Response) (err error) {
+	files, err := dir.Readdir(-1)
 	if err != nil {
 		return
 	}
diff --git a/middleware/timeout.go b/middleware/timeout.go
index d56e463..fb8ae42 100644
--- a/middleware/timeout.go
+++ b/middleware/timeout.go
@@ -92,6 +92,15 @@ func (t echoHandlerFuncWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Reques
 	originalWriter := t.ctx.Response().Writer
 	t.ctx.Response().Writer = rw
 
+	// in case of panic we restore original writer and call panic again
+	// so it could be handled with global middleware Recover()
+	defer func() {
+		if err := recover(); err != nil {
+			t.ctx.Response().Writer = originalWriter
+			panic(err)
+		}
+	}()
+
 	err := t.handler(t.ctx)
 	if ctxErr := r.Context().Err(); ctxErr == context.DeadlineExceeded {
 		if err != nil && t.errHandler != nil {


Test patch:
diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go
index 205721a..1a02659 100644
--- a/middleware/jwt_test.go
+++ b/middleware/jwt_test.go
@@ -1,6 +1,7 @@
 package middleware
 
 import (
+	"errors"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -178,6 +179,14 @@ func TestJWT(t *testing.T) {
 			hdrCookie: "jwt=" + token,
 			info:      "Valid cookie method",
 		},
+		{
+			config: JWTConfig{
+				SigningKey:  validKey,
+				TokenLookup: "query:jwt,cookie:jwt",
+			},
+			hdrCookie: "jwt=" + token,
+			info:      "Multiple jwt lookuop",
+		},
 		{
 			config: JWTConfig{
 				SigningKey:  validKey,
@@ -220,6 +229,35 @@ func TestJWT(t *testing.T) {
 			expErrCode: http.StatusBadRequest,
 			info:       "Empty form field",
 		},
+		{
+			hdrAuth: validAuth,
+			config: JWTConfig{
+				KeyFunc: func(*jwt.Token) (interface{}, error) {
+					return validKey, nil
+				},
+			},
+			info: "Valid JWT with a valid key using a user-defined KeyFunc",
+		},
+		{
+			hdrAuth: validAuth,
+			config: JWTConfig{
+				KeyFunc: func(*jwt.Token) (interface{}, error) {
+					return invalidKey, nil
+				},
+			},
+			expErrCode: http.StatusUnauthorized,
+			info:       "Valid JWT with an invalid key using a user-defined KeyFunc",
+		},
+		{
+			hdrAuth: validAuth,
+			config: JWTConfig{
+				KeyFunc: func(*jwt.Token) (interface{}, error) {
+					return nil, errors.New("faulty KeyFunc")
+				},
+			},
+			expErrCode: http.StatusUnauthorized,
+			info:       "Token verification does not pass using a user-defined KeyFunc",
+		},
 	} {
 		if tc.reqURL == "" {
 			tc.reqURL = "/"
diff --git a/middleware/key_auth_test.go b/middleware/key_auth_test.go
index b874898..476b402 100644
--- a/middleware/key_auth_test.go
+++ b/middleware/key_auth_test.go
@@ -1,9 +1,9 @@
 package middleware
 
 import (
+	"errors"
 	"net/http"
 	"net/http/httptest"
-	"net/url"
 	"strings"
 	"testing"
 
@@ -11,65 +11,225 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func testKeyValidator(key string, c echo.Context) (bool, error) {
+	switch key {
+	case "valid-key":
+		return true, nil
+	case "error-key":
+		return false, errors.New("some user defined error")
+	default:
+		return false, nil
+	}
+}
+
 func TestKeyAuth(t *testing.T) {
+	handlerCalled := false
+	handler := func(c echo.Context) error {
+		handlerCalled = true
+		return c.String(http.StatusOK, "test")
+	}
+	middlewareChain := KeyAuth(testKeyValidator)(handler)
+
 	e := echo.New()
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set(echo.HeaderAuthorization, "Bearer valid-key")
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
-	config := KeyAuthConfig{
-		Validator: func(key string, c echo.Context) (bool, error) {
-			return key == "valid-key", nil
-		},
-	}
-	h := KeyAuthWithConfig(config)(func(c echo.Context) error {
-		return c.String(http.StatusOK, "test")
-	})
 
-	assert := assert.New(t)
+	err := middlewareChain(c)
 
-	// Valid key
-	auth := DefaultKeyAuthConfig.AuthScheme + " " + "valid-key"
-	req.Header.Set(echo.HeaderAuthorization, auth)
-	assert.NoError(h(c))
+	assert.NoError(t, err)
+	assert.True(t, handlerCalled)
+}
 
-	// Invalid key
-	auth = DefaultKeyAuthConfig.AuthScheme + " " + "invalid-key"
-	req.Header.Set(echo.HeaderAuthorization, auth)
-	he := h(c).(*echo.HTTPError)
-	assert.Equal(http.StatusUnauthorized, he.Code)
+func TestKeyAuthWithConfig(t *testing.T) {
+	var testCases = []struct {
+		name                string
+		givenRequestFunc    func() *http.Request
+		givenRequest        func(req *http.Request)
+		whenConfig          func(conf *KeyAuthConfig)
+		expectHandlerCalled bool
+		expectError         string
+	}{
+		{
+			name: "ok, defaults, key from header",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set(echo.HeaderAuthorization, "Bearer valid-key")
+			},
+			expectHandlerCalled: true,
+		},
+		{
+			name: "ok, custom skipper",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set(echo.HeaderAuthorization, "Bearer error-key")
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.Skipper = func(context echo.Context) bool {
+					return true
+				}
+			},
+			expectHandlerCalled: true,
+		},
+		{
+			name: "nok, defaults, invalid key from header, Authorization: Bearer",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set(echo.HeaderAuthorization, "Bearer invalid-key")
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=401, message=Unauthorized",
+		},
+		{
+			name: "nok, defaults, invalid scheme in header",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set(echo.HeaderAuthorization, "Bear valid-key")
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=400, message=invalid key in the request header",
+		},
+		{
+			name:                "nok, defaults, missing header",
+			givenRequest:        func(req *http.Request) {},
+			expectHandlerCalled: false,
+			expectError:         "code=400, message=missing key in request header",
+		},
+		{
+			name: "ok, custom key lookup, header",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set("API-Key", "valid-key")
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "header:API-Key"
+			},
+			expectHandlerCalled: true,
+		},
+		{
+			name: "nok, custom key lookup, missing header",
+			givenRequest: func(req *http.Request) {
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "header:API-Key"
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=400, message=missing key in request header",
+		},
+		{
+			name: "ok, custom key lookup, query",
+			givenRequest: func(req *http.Request) {
+				q := req.URL.Query()
+				q.Add("key", "valid-key")
+				req.URL.RawQuery = q.Encode()
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "query:key"
+			},
+			expectHandlerCalled: true,
+		},
+		{
+			name: "nok, custom key lookup, missing query param",
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "query:key"
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=400, message=missing key in the query string",
+		},
+		{
+			name: "ok, custom key lookup, form",
+			givenRequestFunc: func() *http.Request {
+				req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("key=valid-key"))
+				req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
+				return req
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "form:key"
+			},
+			expectHandlerCalled: true,
+		},
+		{
+			name: "nok, custom key lookup, missing key in form",
+			givenRequestFunc: func() *http.Request {
+				req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("xxx=valid-key"))
+				req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
+				return req
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "form:key"
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=400, message=missing key in the form",
+		},
+		{
+			name: "nok, custom errorHandler, error from extractor",
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "header:token"
+				conf.ErrorHandler = func(err error, context echo.Context) error {
+					httpError := echo.NewHTTPError(http.StatusTeapot, "custom")
+					httpError.Internal = err
+					return httpError
+				}
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=418, message=custom, internal=missing key in request header",
+		},
+		{
+			name: "nok, custom errorHandler, error from validator",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set(echo.HeaderAuthorization, "Bearer error-key")
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.ErrorHandler = func(err error, context echo.Context) error {
+					httpError := echo.NewHTTPError(http.StatusTeapot, "custom")
+					httpError.Internal = err
+					return httpError
+				}
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=418, message=custom, internal=some user defined error",
+		},
+		{
+			name: "nok, defaults, error from validator",
+			givenRequest: func(req *http.Request) {
+				req.Header.Set(echo.HeaderAuthorization, "Bearer error-key")
+			},
+			whenConfig:          func(conf *KeyAuthConfig) {},
+			expectHandlerCalled: false,
+			expectError:         "code=401, message=invalid key, internal=some user defined error",
+		},
+	}
 
-	// Missing Authorization header
-	req.Header.Del(echo.HeaderAuthorization)
-	he = h(c).(*echo.HTTPError)
-	assert.Equal(http.StatusBadRequest, he.Code)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			handlerCalled := false
+			handler := func(c echo.Context) error {
+				handlerCalled = true
+				return c.String(http.StatusOK, "test")
+			}
+			config := KeyAuthConfig{
+				Validator: testKeyValidator,
+			}
+			if tc.whenConfig != nil {
+				tc.whenConfig(&config)
+			}
+			middlewareChain := KeyAuthWithConfig(config)(handler)
 
-	// Key from custom header
-	config.KeyLookup = "header:API-Key"
-	h = KeyAuthWithConfig(config)(func(c echo.Context) error {
-		return c.String(http.StatusOK, "test")
-	})
-	req.Header.Set("API-Key", "valid-key")
-	assert.NoError(h(c))
+			e := echo.New()
+			req := httptest.NewRequest(http.MethodGet, "/", nil)
+			if tc.givenRequestFunc != nil {
+				req = tc.givenRequestFunc()
+			}
+			if tc.givenRequest != nil {
+				tc.givenRequest(req)
+			}
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
 
-	// Key from query string
-	config.KeyLookup = "query:key"
-	h = KeyAuthWithConfig(config)(func(c echo.Context) error {
-		return c.String(http.StatusOK, "test")
-	})
-	q := req.URL.Query()
-	q.Add("key", "valid-key")
-	req.URL.RawQuery = q.Encode()
-	assert.NoError(h(c))
+			err := middlewareChain(c)
 
-	// Key from form
-	config.KeyLookup = "form:key"
-	h = KeyAuthWithConfig(config)(func(c echo.Context) error {
-		return c.String(http.StatusOK, "test")
-	})
-	f := make(url.Values)
-	f.Set("key", "valid-key")
-	req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
-	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
-	c = e.NewContext(req, rec)
-	assert.NoError(h(c))
+			assert.Equal(t, tc.expectHandlerCalled, handlerCalled)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
 }
diff --git a/middleware/static_1_16_test.go b/middleware/static_1_16_test.go
new file mode 100644
index 0000000..53e02f7
--- /dev/null
+++ b/middleware/static_1_16_test.go
@@ -0,0 +1,106 @@
+// +build go1.16
+
+package middleware
+
+import (
+	"io/fs"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"testing/fstest"
+
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestStatic_CustomFS(t *testing.T) {
+	var testCases = []struct {
+		name           string
+		filesystem     fs.FS
+		root           string
+		whenURL        string
+		expectContains string
+		expectCode     int
+	}{
+		{
+			name:           "ok, serve index with Echo message",
+			whenURL:        "/",
+			filesystem:     os.DirFS("../_fixture"),
+			expectCode:     http.StatusOK,
+			expectContains: "<title>Echo</title>",
+		},
+
+		{
+			name:           "ok, serve index with Echo message",
+			whenURL:        "/_fixture/",
+			filesystem:     os.DirFS(".."),
+			expectCode:     http.StatusOK,
+			expectContains: "<title>Echo</title>",
+		},
+		{
+			name:    "ok, serve file from map fs",
+			whenURL: "/file.txt",
+			filesystem: fstest.MapFS{
+				"file.txt": &fstest.MapFile{Data: []byte("file.txt is ok")},
+			},
+			expectCode:     http.StatusOK,
+			expectContains: "file.txt is ok",
+		},
+		{
+			name:       "nok, missing file in map fs",
+			whenURL:    "/file.txt",
+			expectCode: http.StatusNotFound,
+			filesystem: fstest.MapFS{
+				"file2.txt": &fstest.MapFile{Data: []byte("file2.txt is ok")},
+			},
+		},
+		{
+			name:    "nok, file is not a subpath of root",
+			whenURL: `/../../secret.txt`,
+			root:    "/nested/folder",
+			filesystem: fstest.MapFS{
+				"secret.txt": &fstest.MapFile{Data: []byte("this is a secret")},
+			},
+			expectCode: http.StatusNotFound,
+		},
+		{
+			name:       "nok, backslash is forbidden",
+			whenURL:    `/..\..\secret.txt`,
+			expectCode: http.StatusNotFound,
+			root:       "/nested/folder",
+			filesystem: fstest.MapFS{
+				"secret.txt": &fstest.MapFile{Data: []byte("this is a secret")},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
+
+			config := StaticConfig{
+				Root:       ".",
+				Filesystem: http.FS(tc.filesystem),
+			}
+
+			if tc.root != "" {
+				config.Root = tc.root
+			}
+
+			middlewareFunc := StaticWithConfig(config)
+			e.Use(middlewareFunc)
+
+			req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
+			rec := httptest.NewRecorder()
+
+			e.ServeHTTP(rec, req)
+
+			assert.Equal(t, tc.expectCode, rec.Code)
+			if tc.expectContains != "" {
+				responseBody := rec.Body.String()
+				assert.Contains(t, responseBody, tc.expectContains)
+			}
+		})
+	}
+}
diff --git a/middleware/static_test.go b/middleware/static_test.go
index 8c0c97d..af6641f 100644
--- a/middleware/static_test.go
+++ b/middleware/static_test.go
@@ -94,6 +94,32 @@ func TestStatic(t *testing.T) {
 			expectCode:     http.StatusNotFound,
 			expectContains: "{\"message\":\"Not Found\"}\n",
 		},
+		{
+			name:           "ok, do not serve file, when a handler took care of the request",
+			whenURL:        "/regular-handler",
+			expectCode:     http.StatusOK,
+			expectContains: "ok",
+		},
+		{
+			name: "nok, when html5 fail if the index file does not exist",
+			givenConfig: &StaticConfig{
+				Root:  "../_fixture",
+				HTML5: true,
+				Index: "missing.html",
+			},
+			whenURL:    "/random",
+			expectCode: http.StatusInternalServerError,
+		},
+		{
+			name: "ok, serve from http.FileSystem",
+			givenConfig: &StaticConfig{
+				Root:       "_fixture",
+				Filesystem: http.Dir(".."),
+			},
+			whenURL:        "/",
+			expectCode:     http.StatusOK,
+			expectContains: "<title>Echo</title>",
+		},
 	}
 
 	for _, tc := range testCases {
@@ -115,6 +141,9 @@ func TestStatic(t *testing.T) {
 			} else {
 				// middleware is on root level
 				e.Use(middlewareFunc)
+				e.GET("/regular-handler", func(c echo.Context) error {
+					return c.String(http.StatusOK, "ok")
+				})
 			}
 
 			req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)