Repository: labstack/echo
Pull request #1866 (2021-05-06 11:34:19)

feat: Bind data using headers as source
Currently, echo supports binding data from query, path or body.
Sometimes we need to read bind data from headers. It would be nice to
automatically bind those using the `bindData` func, which is already
well prepared to accept `http.Header`.

I didn't add this to the `Bind` func, so this will not happen
automatically. Main reason is backwards compatability. It might be
confusing if variables are bound from headers when upgrading, and might
even have become a security issue as pointed out in #1670.

noseglid: I realize we should add documentation over this seeing as this is a new feature and all. But I wanted to get the PR up here to see if this is something you'd be willing to accept.  And if so, we can discuss next steps.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3117b8..f4a7476 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
 # Changelog
 
+## v4.3.0 - 2021-05-08
+
+**Important notes**
+
+* Route matching has improvements for following cases:
+  1. Correctly match routes with parameter part as last part of route (with trailing backslash)
+  2. Considering handlers when resolving routes and search for matching http method handler
+* Echo minimal Go version is now 1.13. 
+
+**Fixes**
+
+* When url ends with slash first param route is the match [#1804](https://github.com/labstack/echo/pull/1812)
+* Router should check if node is suitable as matching route by path+method and if not then continue search in tree [#1808](https://github.com/labstack/echo/issues/1808)
+* Fix timeout middleware not writing response correctly when handler panics [#1864](https://github.com/labstack/echo/pull/1864)
+* Fix binder not working with embedded pointer structs [#1861](https://github.com/labstack/echo/pull/1861)
+* Add Go 1.16 to CI and drop 1.12 specific code [#1850](https://github.com/labstack/echo/pull/1850)
+
+**Enhancements**
+
+* Make KeyFunc public in JWT middleware [#1756](https://github.com/labstack/echo/pull/1756)
+* Add support for optional filesystem to the static middleware [#1797](https://github.com/labstack/echo/pull/1797)
+* Add a custom error handler to key-auth middleware [#1847](https://github.com/labstack/echo/pull/1847)
+* Allow JWT token to be looked up from multiple sources [#1845](https://github.com/labstack/echo/pull/1845)
+
 ## v4.2.2 - 2021-04-07
 
 **Fixes**
diff --git a/bind.go b/bind.go
index 08d3989..47947ce 100644
--- a/bind.go
+++ b/bind.go
@@ -97,6 +97,14 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
 	return nil
 }
 
+// BindHeaders binds HTTP headers to a bindable object
+func (b *DefaultBinder) BindHeaders(c Context, i interface{}) error {
+	if err := b.bindData(i, c.Request().Header, "header"); err != nil {
+		return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
+	}
+	return nil
+}
+
 // Bind implements the `Binder#Bind` function.
 // Binding is done in following order: 1) path params; 2) query params; 3) request body. Each step COULD override previous
 // step binded values. For single source binding use their own methods BindBody, BindQueryParams, BindPathParams.
@@ -134,7 +142,7 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
 
 	// !struct
 	if typ.Kind() != reflect.Struct {
-		if tag == "param" || tag == "query" {
+		if tag == "param" || tag == "query" || tag == "header" {
 			// incompatible type, data is probably to be found in the body
 			return nil
 		}
@@ -144,11 +152,20 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
 	for i := 0; i < typ.NumField(); i++ {
 		typeField := typ.Field(i)
 		structField := val.Field(i)
+		if typeField.Anonymous {
+			if structField.Kind() == reflect.Ptr {
+				structField = structField.Elem()
+			}
+		}
 		if !structField.CanSet() {
 			continue
 		}
 		structFieldKind := structField.Kind()
 		inputFieldName := typeField.Tag.Get(tag)
+		if typeField.Anonymous && structField.Kind() == reflect.Struct && inputFieldName != "" {
+			// if anonymous struct with query/param/form tags, report an error
+			return errors.New("query/param/form tags are not allowed with anonymous struct field")
+		}
 
 		if inputFieldName == "" {
 			// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags).
diff --git a/echo.go b/echo.go
index a24e397..dd0cbf3 100644
--- a/echo.go
+++ b/echo.go
@@ -234,7 +234,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.2.2"
+	Version = "4.3.0"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
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/bind_test.go b/bind_test.go
index 7339803..4ed8dbb 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -100,6 +100,9 @@ type (
 	Struct      struct {
 		Foo string
 	}
+	Bar struct {
+		Baz int `json:"baz" query:"baz"`
+	}
 )
 
 func (t *Timestamp) UnmarshalParam(src string) error {
@@ -266,6 +269,37 @@ func TestBindQueryParamsCaseSensitivePrioritized(t *testing.T) {
 	}
 }
 
+func TestBindHeaderParam(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set("Name", "Jon Doe")
+	req.Header.Set("Id", "2")
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	u := new(user)
+	err := (&DefaultBinder{}).BindHeaders(c, u)
+	if assert.NoError(t, err) {
+		assert.Equal(t, 2, u.ID)
+		assert.Equal(t, "Jon Doe", u.Name)
+	}
+}
+
+func TestBindHeaderParamBadType(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set("Id", "salamander")
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	u := new(user)
+	err := (&DefaultBinder{}).BindHeaders(c, u)
+	assert.Error(t, err)
+
+	httpErr, ok := err.(*HTTPError)
+	if assert.True(t, ok) {
+		assert.Equal(t, http.StatusBadRequest, httpErr.Code)
+	}
+}
+
 func TestBindUnmarshalParam(t *testing.T) {
 	e := New()
 	req := httptest.NewRequest(http.MethodGet, "/?ts=2016-12-06T19:09:05Z&sa=one,two,three&ta=2016-12-06T19:09:05Z&ta=2016-12-06T19:09:05Z&ST=baz", nil)
@@ -330,6 +364,46 @@ func TestBindUnmarshalParamPtr(t *testing.T) {
 	}
 }
 
+func TestBindUnmarshalParamAnonymousFieldPtr(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/?baz=1", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	result := struct {
+		*Bar
+	}{&Bar{}}
+	err := c.Bind(&result)
+	if assert.NoError(t, err) {
+		assert.Equal(t, 1, result.Baz)
+	}
+}
+
+func TestBindUnmarshalParamAnonymousFieldPtrNil(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/?baz=1", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	result := struct {
+		*Bar
+	}{}
+	err := c.Bind(&result)
+	if assert.NoError(t, err) {
+		assert.Nil(t, result.Bar)
+	}
+}
+
+func TestBindUnmarshalParamAnonymousFieldPtrCustomTag(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, `/?bar={"baz":100}&baz=1`, nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	result := struct {
+		*Bar `json:"bar" query:"bar"`
+	}{&Bar{}}
+	err := c.Bind(&result)
+	assert.Contains(t, err.Error(), "query/param/form tags are not allowed with anonymous struct field")
+}
+
 func TestBindUnmarshalTextPtr(t *testing.T) {
 	e := New()
 	req := httptest.NewRequest(GET, "/?ts=2016-12-06T19:09:05Z", nil)
diff --git a/echo_test.go b/echo_test.go
index ba49883..e5bd371 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -24,8 +24,8 @@ import (
 
 type (
 	user struct {
-		ID   int    `json:"id" xml:"id" form:"id" query:"id" param:"id"`
-		Name string `json:"name" xml:"name" form:"name" query:"name" param:"name"`
+		ID   int    `json:"id" xml:"id" form:"id" query:"id" param:"id" header:"id"`
+		Name string `json:"name" xml:"name" form:"name" query:"name" param:"name" header:"name"`
 	}
 )
 
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)