Repository: labstack/echo
Pull request #1847 (2021-04-12 09:27:03)

add a custom error handler to key-auth middleware
link to https://github.com/labstack/echo/issues/1846

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=h1) Report
> Merging [#1847](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=desc) (4010207) into [master](https://codecov.io/gh/labstack/echo/commit/bb7f2223bbddee817a1e8a940d186a7328025f6e?el=desc) (bb7f222) will **decrease** coverage by `0.09%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1847/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i)](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1847      +/-   ##
==========================================
- Coverage   89.57%   89.47%   -0.10%     
==========================================
  Files          32       32              
  Lines        2686     2690       +4     
==========================================
+ Hits         2406     2407       +1     
- Misses        180      183       +3     
  Partials      100      100              
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [middleware/key\_auth.go](https://codecov.io/gh/labstack/echo/pull/1847/diff?src=pr&el=tree#diff-bWlkZGxld2FyZS9rZXlfYXV0aC5nbw==) | `63.79% <0.00%> (-2.88%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=footer). Last update [bb7f222...4010207](https://codecov.io/gh/labstack/echo/pull/1847?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

hyacinthus: I'll update doc after pr accepted.

And it's hard to add a test to this patch...

Patch:
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index fb8c502..ec55175 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -25,7 +25,9 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        go: [1.12, 1.13, 1.14, 1.15]
+        # Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy
+        # Echo tests with last four major releases
+        go: [1.13, 1.14, 1.15, 1.16]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
@@ -59,7 +61,7 @@ jobs:
           go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
 
       - name: Upload coverage to Codecov
-        if: success() && matrix.go == 1.15 && matrix.os == 'ubuntu-latest'
+        if: success() && matrix.go == 1.16 && matrix.os == 'ubuntu-latest'
         uses: codecov/codecov-action@v1
         with:
           token:
@@ -69,7 +71,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        go: [1.15]
+        go: [1.16]
     name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1be77a..a3117b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 # Changelog
 
-## v4.2.2 - 2020-04-07
+## v4.2.2 - 2021-04-07
 
 **Fixes**
 
@@ -10,7 +10,7 @@
 * Fix panic in redirect middleware on short host name (#1813)
 * Fix timeout middleware docs (#1836)
 
-## v4.2.1 - 2020-03-08
+## v4.2.1 - 2021-03-08
 
 **Important notes**
 
@@ -32,7 +32,7 @@ A performance regression has been fixed, even bringing better performance than b
 This release was made possible by our **contributors**:
 aldas, clwluvw, lammel, Le0tk0k, maciej-jezierski, rkilingr, stffabi, withshubh
 
-## v4.2.0 - 2020-02-11
+## v4.2.0 - 2021-02-11
 
 **Important notes**
 
diff --git a/LICENSE b/LICENSE
index b5b006b..c46d010 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2017 LabStack
+Copyright (c) 2021 LabStack
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/middleware/csrf.go b/middleware/csrf.go
index 60f809a..7804997 100644
--- a/middleware/csrf.go
+++ b/middleware/csrf.go
@@ -110,7 +110,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 	if config.CookieMaxAge == 0 {
 		config.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge
 	}
-	if config.CookieSameSite == SameSiteNoneMode {
+	if config.CookieSameSite == http.SameSiteNoneMode {
 		config.CookieSecure = true
 	}
 
diff --git a/middleware/csrf_samesite.go b/middleware/csrf_samesite.go
deleted file mode 100644
index 9a27dc4..0000000
--- a/middleware/csrf_samesite.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// +build go1.13
-
-package middleware
-
-import (
-	"net/http"
-)
-
-const (
-	// SameSiteNoneMode required to be redefined for Go 1.12 support (see #1524)
-	SameSiteNoneMode http.SameSite = http.SameSiteNoneMode
-)
diff --git a/middleware/csrf_samesite_1.12.go b/middleware/csrf_samesite_1.12.go
deleted file mode 100644
index 22076dd..0000000
--- a/middleware/csrf_samesite_1.12.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// +build !go1.13
-
-package middleware
-
-import (
-	"net/http"
-)
-
-const (
-	// SameSiteNoneMode required to be redefined for Go 1.12 support (see #1524)
-	SameSiteNoneMode http.SameSite = 4
-)
diff --git a/middleware/jwt.go b/middleware/jwt.go
index da00ea5..fab4d6f 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,21 +155,8 @@ 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
@@ -196,11 +197,11 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 			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 +226,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/proxy.go b/middleware/proxy.go
index 6f01f3a..6cfd673 100644
--- a/middleware/proxy.go
+++ b/middleware/proxy.go
@@ -1,13 +1,16 @@
 package middleware
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"math/rand"
 	"net"
 	"net/http"
+	"net/http/httputil"
 	"net/url"
 	"regexp"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -264,3 +267,37 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
 		}
 	}
 }
+
+// StatusCodeContextCanceled is a custom HTTP status code for situations
+// where a client unexpectedly closed the connection to the server.
+// As there is no standard error code for "client closed connection", but
+// various well-known HTTP clients and server implement this HTTP code we use
+// 499 too instead of the more problematic 5xx, which does not allow to detect this situation
+const StatusCodeContextCanceled = 499
+
+func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
+	proxy := httputil.NewSingleHostReverseProxy(tgt.URL)
+	proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {
+		desc := tgt.URL.String()
+		if tgt.Name != "" {
+			desc = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String())
+		}
+		// If the client canceled the request (usually by closing the connection), we can report a
+		// client error (4xx) instead of a server error (5xx) to correctly identify the situation.
+		// The Go standard library (at of late 2020) wraps the exported, standard
+		// context.Canceled error with unexported garbage value requiring a substring check, see
+		// https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430
+		if err == context.Canceled || strings.Contains(err.Error(), "operation was canceled") {
+			httpError := echo.NewHTTPError(StatusCodeContextCanceled, fmt.Sprintf("client closed connection: %v", err))
+			httpError.Internal = err
+			c.Set("_error", httpError)
+		} else {
+			httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
+			httpError.Internal = err
+			c.Set("_error", httpError)
+		}
+	}
+	proxy.Transport = config.Transport
+	proxy.ModifyResponse = config.ModifyResponse
+	return proxy
+}
diff --git a/middleware/proxy_1_11.go b/middleware/proxy_1_11.go
deleted file mode 100644
index 17d142d..0000000
--- a/middleware/proxy_1_11.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// +build go1.11
-
-package middleware
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"net/http/httputil"
-	"strings"
-
-	"github.com/labstack/echo/v4"
-)
-
-// StatusCodeContextCanceled is a custom HTTP status code for situations
-// where a client unexpectedly closed the connection to the server.
-// As there is no standard error code for "client closed connection", but
-// various well-known HTTP clients and server implement this HTTP code we use
-// 499 too instead of the more problematic 5xx, which does not allow to detect this situation
-const StatusCodeContextCanceled = 499
-
-func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
-	proxy := httputil.NewSingleHostReverseProxy(tgt.URL)
-	proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {
-		desc := tgt.URL.String()
-		if tgt.Name != "" {
-			desc = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String())
-		}
-		// If the client canceled the request (usually by closing the connection), we can report a
-		// client error (4xx) instead of a server error (5xx) to correctly identify the situation.
-		// The Go standard library (at of late 2020) wraps the exported, standard
-		// context.Canceled error with unexported garbage value requiring a substring check, see
-		// https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430
-		if err == context.Canceled || strings.Contains(err.Error(), "operation was canceled") {
-			httpError := echo.NewHTTPError(StatusCodeContextCanceled, fmt.Sprintf("client closed connection: %v", err))
-			httpError.Internal = err
-			c.Set("_error", httpError)
-		} else {
-			httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
-			httpError.Internal = err
-			c.Set("_error", httpError)
-		}
-	}
-	proxy.Transport = config.Transport
-	proxy.ModifyResponse = config.ModifyResponse
-	return proxy
-}
diff --git a/middleware/proxy_1_11_n.go b/middleware/proxy_1_11_n.go
deleted file mode 100644
index 9a78929..0000000
--- a/middleware/proxy_1_11_n.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// +build !go1.11
-
-package middleware
-
-import (
-	"net/http"
-	"net/http/httputil"
-
-	"github.com/labstack/echo/v4"
-)
-
-func proxyHTTP(t *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
-	return httputil.NewSingleHostReverseProxy(t.URL)
-}
diff --git a/middleware/timeout.go b/middleware/timeout.go
index 5d23ff4..d56e463 100644
--- a/middleware/timeout.go
+++ b/middleware/timeout.go
@@ -1,5 +1,3 @@
-// +build go1.13
-
 package middleware
 
 import (
diff --git a/router.go b/router.go
index 2dd09fa..5b2474b 100644
--- a/router.go
+++ b/router.go
@@ -23,6 +23,10 @@ type (
 		methodHandler  *methodHandler
 		paramChild     *node
 		anyChild       *node
+		// isLeaf indicates that node does not have child routes
+		isLeaf bool
+		// isHandler indicates that node has at least one handler registered to it
+		isHandler bool
 	}
 	kind          uint8
 	children      []*node
@@ -50,6 +54,20 @@ const (
 	anyLabel   = byte('*')
 )
 
+func (m *methodHandler) isHandler() bool {
+	return m.connect != nil ||
+		m.delete != nil ||
+		m.get != nil ||
+		m.head != nil ||
+		m.options != nil ||
+		m.patch != nil ||
+		m.post != nil ||
+		m.propfind != nil ||
+		m.put != nil ||
+		m.trace != nil ||
+		m.report != nil
+}
+
 // NewRouter returns a new Router instance.
 func NewRouter(e *Echo) *Router {
 	return &Router{
@@ -73,6 +91,11 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 	pnames := []string{} // Param names
 	ppath := path        // Pristine path
 
+	if h == nil && r.echo.Logger != nil {
+		// FIXME: in future we should return error
+		r.echo.Logger.Errorf("Adding route without handler function: %v:%v", method, path)
+	}
+
 	for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
 		if path[i] == ':' {
 			j := i + 1
@@ -86,6 +109,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 			i, lcpIndex = j, len(path)
 
 			if i == lcpIndex {
+				// path node is last fragment of route path. ie. `/users/:id`
 				r.insert(method, path[:i], h, paramKind, ppath, pnames)
 			} else {
 				r.insert(method, path[:i], nil, paramKind, "", nil)
@@ -136,6 +160,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 				currentNode.ppath = ppath
 				currentNode.pnames = pnames
 			}
+			currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
 		} else if lcpLen < prefixLen {
 			// Split node
 			n := newNode(
@@ -149,7 +174,6 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 				currentNode.paramChild,
 				currentNode.anyChild,
 			)
-
 			// Update parent path for all children to new node
 			for _, child := range currentNode.staticChildren {
 				child.parent = n
@@ -171,6 +195,8 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 			currentNode.pnames = nil
 			currentNode.paramChild = nil
 			currentNode.anyChild = nil
+			currentNode.isLeaf = false
+			currentNode.isHandler = false
 
 			// Only Static children could reach here
 			currentNode.addStaticChild(n)
@@ -188,6 +214,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 				// Only Static children could reach here
 				currentNode.addStaticChild(n)
 			}
+			currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
 		} else if lcpLen < searchLen {
 			search = search[lcpLen:]
 			c := currentNode.findChildWithLabel(search[0])
@@ -207,6 +234,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
 			case anyKind:
 				currentNode.anyChild = n
 			}
+			currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
 		} else {
 			// Node already exists
 			if h != nil {
@@ -233,6 +261,8 @@ func newNode(t kind, pre string, p *node, sc children, mh *methodHandler, ppath
 		methodHandler:  mh,
 		paramChild:     paramChildren,
 		anyChild:       anyChildren,
+		isLeaf:         sc == nil && paramChildren == nil && anyChildren == nil,
+		isHandler:      mh.isHandler(),
 	}
 }
 
@@ -289,6 +319,12 @@ func (n *node) addHandler(method string, h HandlerFunc) {
 	case REPORT:
 		n.methodHandler.report = h
 	}
+
+	if h != nil {
+		n.isHandler = true
+	} else {
+		n.isHandler = n.methodHandler.isHandler()
+	}
 }
 
 func (n *node) findHandler(method string) HandlerFunc {
@@ -343,6 +379,8 @@ func (r *Router) Find(method, path string, c Context) {
 	currentNode := r.tree // Current node as root
 
 	var (
+		previousBestMatchNode *node
+		matchedHandler        HandlerFunc
 		// search stores the remaining path to check for match. By each iteration we move from start of path to end of the path
 		// and search value gets shorter and shorter.
 		search      = path
@@ -362,10 +400,11 @@ func (r *Router) Find(method, path string, c Context) {
 		valid = currentNode != nil
 
 		// Next node type by priority
-		// NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is
-		// always `static` or `any`
-		// If this is changed then for any route next kind would be `static` and this statement should be changed
-		nextNodeKind = previous.kind + 1
+		if previous.kind == anyKind {
+			nextNodeKind = staticKind
+		} else {
+			nextNodeKind = previous.kind + 1
+		}
 
 		if fromKind == staticKind {
 			// when backtracking is done from static kind block we did not change search so nothing to restore
@@ -380,6 +419,7 @@ func (r *Router) Find(method, path string, c Context) {
 			// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
 			// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
 			searchIndex -= len(paramValues[paramIndex])
+			paramValues[paramIndex] = ""
 		}
 		search = path[searchIndex:]
 		return
@@ -421,7 +461,7 @@ func (r *Router) Find(method, path string, c Context) {
 				//	goto Any
 			} else {
 				// Not found (this should never be possible for static node we are looking currently)
-				return
+				break
 			}
 		}
 
@@ -429,9 +469,17 @@ func (r *Router) Find(method, path string, c Context) {
 		search = search[lcpLen:]
 		searchIndex = searchIndex + lcpLen
 
-		// Finish routing if no remaining search and we are on an leaf node
-		if search == "" && currentNode.ppath != "" {
-			break
+		// Finish routing if no remaining search and we are on a node with handler and matching method type
+		if search == "" && currentNode.isHandler {
+			// check if current node has handler registered for http method we are looking for. we store currentNode as
+			// best matching in case we do no find no more routes matching this path+method
+			if previousBestMatchNode == nil {
+				previousBestMatchNode = currentNode
+			}
+			if h := currentNode.findHandler(method); h != nil {
+				matchedHandler = h
+				break
+			}
 		}
 
 		// Static node
@@ -446,10 +494,16 @@ func (r *Router) Find(method, path string, c Context) {
 		// Param node
 		if child := currentNode.paramChild; search != "" && child != nil {
 			currentNode = child
-			// FIXME: when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
-			i, l := 0, len(search)
-			for ; i < l && search[i] != '/'; i++ {
+			i := 0
+			l := len(search)
+			if currentNode.isLeaf {
+				// when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
+				i = l
+			} else {
+				for ; i < l && search[i] != '/'; i++ {
+				}
 			}
+
 			paramValues[paramIndex] = search[:i]
 			paramIndex++
 			search = search[i:]
@@ -463,29 +517,50 @@ func (r *Router) Find(method, path string, c Context) {
 			// If any node is found, use remaining path for paramValues
 			currentNode = child
 			paramValues[len(currentNode.pnames)-1] = search
-			break
+			// update indexes/search in case we need to backtrack when no handler match is found
+			paramIndex++
+			searchIndex += +len(search)
+			search = ""
+
+			// check if current node has handler registered for http method we are looking for. we store currentNode as
+			// best matching in case we do no find no more routes matching this path+method
+			if previousBestMatchNode == nil {
+				previousBestMatchNode = currentNode
+			}
+			if h := currentNode.findHandler(method); h != nil {
+				matchedHandler = h
+				break
+			}
 		}
 
 		// Let's backtrack to the first possible alternative node of the decision path
 		nk, ok := backtrackToNextNodeKind(anyKind)
 		if !ok {
-			return // No other possibilities on the decision path
+			break // No other possibilities on the decision path
 		} else if nk == paramKind {
 			goto Param
 		} else if nk == anyKind {
 			goto Any
 		} else {
 			// Not found
-			return
+			break
 		}
 	}
 
-	ctx.handler = currentNode.findHandler(method)
-	ctx.path = currentNode.ppath
-	ctx.pnames = currentNode.pnames
+	if currentNode == nil && previousBestMatchNode == nil {
+		return // nothing matched at all
+	}
 
-	if ctx.handler == nil {
+	if matchedHandler != nil {
+		ctx.handler = matchedHandler
+	} else {
+		// use previous match as basis. although we have no matching handler we have path match.
+		// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)
+		currentNode = previousBestMatchNode
 		ctx.handler = currentNode.checkMethodNotAllowed()
 	}
+	ctx.path = currentNode.ppath
+	ctx.pnames = currentNode.pnames
+
 	return
 }


Test patch:
diff --git a/context_test.go b/context_test.go
index 963c91e..2c61ffb 100644
--- a/context_test.go
+++ b/context_test.go
@@ -464,7 +464,9 @@ func TestContextPath(t *testing.T) {
 	e := New()
 	r := e.Router()
 
-	r.Add(http.MethodGet, "/users/:id", nil)
+	handler := func(c Context) error { return c.String(http.StatusOK, "OK") }
+
+	r.Add(http.MethodGet, "/users/:id", handler)
 	c := e.NewContext(nil, nil)
 	r.Find(http.MethodGet, "/users/1", c)
 
@@ -472,7 +474,7 @@ func TestContextPath(t *testing.T) {
 
 	assert.Equal("/users/:id", c.Path())
 
-	r.Add(http.MethodGet, "/users/:uid/files/:fid", nil)
+	r.Add(http.MethodGet, "/users/:uid/files/:fid", handler)
 	c = e.NewContext(nil, nil)
 	r.Find(http.MethodGet, "/users/1/files/1", c)
 	assert.Equal("/users/:uid/files/:fid", c.Path())
diff --git a/echo_go1.13_test.go b/echo_go1.13_test.go
deleted file mode 100644
index 3c488bc..0000000
--- a/echo_go1.13_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-// +build go1.13
-
-package echo
-
-import (
-	"errors"
-	"net/http"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestHTTPError_Unwrap(t *testing.T) {
-	t.Run("non-internal", func(t *testing.T) {
-		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
-			"code": 12,
-		})
-
-		assert.Nil(t, errors.Unwrap(err))
-	})
-	t.Run("internal", func(t *testing.T) {
-		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
-			"code": 12,
-		})
-		err.SetInternal(errors.New("internal error"))
-		assert.Equal(t, "internal error", errors.Unwrap(err).Error())
-	})
-}
diff --git a/echo_test.go b/echo_test.go
index 58ecea7..ba49883 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -957,6 +957,23 @@ func TestHTTPError(t *testing.T) {
 	})
 }
 
+func TestHTTPError_Unwrap(t *testing.T) {
+	t.Run("non-internal", func(t *testing.T) {
+		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
+			"code": 12,
+		})
+
+		assert.Nil(t, errors.Unwrap(err))
+	})
+	t.Run("internal", func(t *testing.T) {
+		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
+			"code": 12,
+		})
+		err.SetInternal(errors.New("internal error"))
+		assert.Equal(t, "internal error", errors.Unwrap(err).Error())
+	})
+}
+
 func TestDefaultHTTPErrorHandler(t *testing.T) {
 	e := New()
 	e.Debug = true
diff --git a/middleware/csrf_samesite_test.go b/middleware/csrf_samesite_test.go
deleted file mode 100644
index 26c5bc4..0000000
--- a/middleware/csrf_samesite_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// +build go1.13
-
-package middleware
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/labstack/echo/v4"
-	"github.com/stretchr/testify/assert"
-)
-
-// Test for SameSiteModeNone moved to separate file for Go 1.12 support
-func TestCSRFWithSameSiteModeNone(t *testing.T) {
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-
-	csrf := CSRFWithConfig(CSRFConfig{
-		CookieSameSite: SameSiteNoneMode,
-	})
-
-	h := csrf(func(c echo.Context) error {
-		return c.String(http.StatusOK, "test")
-	})
-
-	r := h(c)
-	assert.NoError(t, r)
-	assert.Regexp(t, "SameSite=None", rec.Header()["Set-Cookie"])
-	assert.Regexp(t, "Secure", rec.Header()["Set-Cookie"])
-}
diff --git a/middleware/csrf_test.go b/middleware/csrf_test.go
index ebe4dbc..af1d263 100644
--- a/middleware/csrf_test.go
+++ b/middleware/csrf_test.go
@@ -138,3 +138,23 @@ func TestCSRFWithSameSiteDefaultMode(t *testing.T) {
 	fmt.Println(rec.Header()["Set-Cookie"])
 	assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"])
 }
+
+func TestCSRFWithSameSiteModeNone(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: http.SameSiteNoneMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.Regexp(t, "SameSite=None", rec.Header()["Set-Cookie"])
+	assert.Regexp(t, "Secure", rec.Header()["Set-Cookie"])
+}
diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go
index 205721a..37de31f 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"
@@ -220,6 +221,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/proxy_1_11_test.go b/middleware/proxy_1_11_test.go
deleted file mode 100644
index c3541d5..0000000
--- a/middleware/proxy_1_11_test.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// +build go1.11
-
-package middleware
-
-import (
-	"context"
-	"net/http"
-	"net/http/httptest"
-	"net/url"
-	"sync"
-	"testing"
-	"time"
-
-	"github.com/labstack/echo/v4"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestProxy_1_11(t *testing.T) {
-	// Setup
-	url1, _ := url.Parse("http://127.0.0.1:27121")
-	url2, _ := url.Parse("http://127.0.0.1:27122")
-
-	targets := []*ProxyTarget{
-		{
-			Name: "target 1",
-			URL:  url1,
-		},
-		{
-			Name: "target 2",
-			URL:  url2,
-		},
-	}
-	rb := NewRandomBalancer(nil)
-	// must add targets:
-	for _, target := range targets {
-		assert.True(t, rb.AddTarget(target))
-	}
-
-	// must ignore duplicates:
-	for _, target := range targets {
-		assert.False(t, rb.AddTarget(target))
-	}
-
-	// Random
-	e := echo.New()
-	e.Use(Proxy(rb))
-	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	rec := httptest.NewRecorder()
-
-	// Remote unreachable
-	rec = httptest.NewRecorder()
-	req.URL.Path = "/api/users"
-	e.ServeHTTP(rec, req)
-	assert.Equal(t, "/api/users", req.URL.Path)
-	assert.Equal(t, http.StatusBadGateway, rec.Code)
-}
-
-func TestClientCancelConnectionResultsHTTPCode499(t *testing.T) {
-	var timeoutStop sync.WaitGroup
-	timeoutStop.Add(1)
-	HTTPTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		timeoutStop.Wait() // wait until we have canceled the request
-		w.WriteHeader(http.StatusOK)
-	}))
-	defer HTTPTarget.Close()
-	targetURL, _ := url.Parse(HTTPTarget.URL)
-	target := &ProxyTarget{
-		Name: "target",
-		URL:  targetURL,
-	}
-	rb := NewRandomBalancer(nil)
-	assert.True(t, rb.AddTarget(target))
-	e := echo.New()
-	e.Use(Proxy(rb))
-	rec := httptest.NewRecorder()
-	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	ctx, cancel := context.WithCancel(req.Context())
-	req = req.WithContext(ctx)
-	go func() {
-		time.Sleep(10 * time.Millisecond)
-		cancel()
-	}()
-	e.ServeHTTP(rec, req)
-	timeoutStop.Done()
-	assert.Equal(t, 499, rec.Code)
-}
diff --git a/middleware/proxy_test.go b/middleware/proxy_test.go
index 93daf73..7939fc5 100644
--- a/middleware/proxy_test.go
+++ b/middleware/proxy_test.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io/ioutil"
 	"net"
@@ -9,7 +10,9 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"regexp"
+	"sync"
 	"testing"
+	"time"
 
 	"github.com/labstack/echo/v4"
 	"github.com/stretchr/testify/assert"
@@ -302,3 +305,73 @@ func TestProxyRewriteRegex(t *testing.T) {
 		})
 	}
 }
+
+func TestProxyError(t *testing.T) {
+	// Setup
+	url1, _ := url.Parse("http://127.0.0.1:27121")
+	url2, _ := url.Parse("http://127.0.0.1:27122")
+
+	targets := []*ProxyTarget{
+		{
+			Name: "target 1",
+			URL:  url1,
+		},
+		{
+			Name: "target 2",
+			URL:  url2,
+		},
+	}
+	rb := NewRandomBalancer(nil)
+	// must add targets:
+	for _, target := range targets {
+		assert.True(t, rb.AddTarget(target))
+	}
+
+	// must ignore duplicates:
+	for _, target := range targets {
+		assert.False(t, rb.AddTarget(target))
+	}
+
+	// Random
+	e := echo.New()
+	e.Use(Proxy(rb))
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	// Remote unreachable
+	rec = httptest.NewRecorder()
+	req.URL.Path = "/api/users"
+	e.ServeHTTP(rec, req)
+	assert.Equal(t, "/api/users", req.URL.Path)
+	assert.Equal(t, http.StatusBadGateway, rec.Code)
+}
+
+func TestClientCancelConnectionResultsHTTPCode499(t *testing.T) {
+	var timeoutStop sync.WaitGroup
+	timeoutStop.Add(1)
+	HTTPTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		timeoutStop.Wait() // wait until we have canceled the request
+		w.WriteHeader(http.StatusOK)
+	}))
+	defer HTTPTarget.Close()
+	targetURL, _ := url.Parse(HTTPTarget.URL)
+	target := &ProxyTarget{
+		Name: "target",
+		URL:  targetURL,
+	}
+	rb := NewRandomBalancer(nil)
+	assert.True(t, rb.AddTarget(target))
+	e := echo.New()
+	e.Use(Proxy(rb))
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	ctx, cancel := context.WithCancel(req.Context())
+	req = req.WithContext(ctx)
+	go func() {
+		time.Sleep(10 * time.Millisecond)
+		cancel()
+	}()
+	e.ServeHTTP(rec, req)
+	timeoutStop.Done()
+	assert.Equal(t, 499, rec.Code)
+}
diff --git a/middleware/timeout_test.go b/middleware/timeout_test.go
index 8f8fa30..f9f1826 100644
--- a/middleware/timeout_test.go
+++ b/middleware/timeout_test.go
@@ -1,5 +1,3 @@
-// +build go1.13
-
 package middleware
 
 import (
diff --git a/router_test.go b/router_test.go
index 47e4994..71cedf8 100644
--- a/router_test.go
+++ b/router_test.go
@@ -692,11 +692,11 @@ func TestRouterParam(t *testing.T) {
 			expectRoute: "/users/:id",
 			expectParam: map[string]string{"id": "1"},
 		},
-		{ // FIXME: this documents current implementation (slash at end is problematic)
+		{
 			name:        "route /users/1/ to /users/:id",
 			whenURL:     "/users/1/",
-			expectRoute: nil, // FIXME: should be "/users/:id",
-			expectParam: nil, // FIXME: should be  map[string]string{"id": "1/"},
+			expectRoute: "/users/:id",
+			expectParam: map[string]string{"id": "1/"},
 		},
 	}
 
@@ -716,6 +716,69 @@ func TestRouterParam(t *testing.T) {
 	}
 }
 
+func TestMethodNotAllowedAndNotFound(t *testing.T) {
+	e := New()
+	r := e.router
+
+	// Routes
+	r.Add(http.MethodGet, "/*", handlerFunc)
+	r.Add(http.MethodPost, "/users/:id", handlerFunc)
+
+	var testCases = []struct {
+		name        string
+		whenMethod  string
+		whenURL     string
+		expectRoute interface{}
+		expectParam map[string]string
+		expectError error
+	}{
+		{
+			name:        "exact match for route+method",
+			whenMethod:  http.MethodPost,
+			whenURL:     "/users/1",
+			expectRoute: "/users/:id",
+			expectParam: map[string]string{"id": "1"},
+		},
+		{
+			name:        "matches node but not method. sends 405 from best match node",
+			whenMethod:  http.MethodPut,
+			whenURL:     "/users/1",
+			expectRoute: nil,
+			expectError: ErrMethodNotAllowed,
+		},
+		{
+			name:        "best match is any route up in tree",
+			whenMethod:  http.MethodGet,
+			whenURL:     "/users/1",
+			expectRoute: "/*",
+			expectParam: map[string]string{"*": "users/1"},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := e.NewContext(nil, nil).(*context)
+
+			method := http.MethodGet
+			if tc.whenMethod != "" {
+				method = tc.whenMethod
+			}
+			r.Find(method, tc.whenURL, c)
+			err := c.handler(c)
+
+			if tc.expectError != nil {
+				assert.Equal(t, tc.expectError, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tc.expectRoute, c.Get("path"))
+			for param, expectedValue := range tc.expectParam {
+				assert.Equal(t, expectedValue, c.Param(param))
+			}
+			checkUnusedParamValues(t, c, tc.expectParam)
+		})
+	}
+}
+
 func TestRouterTwoParam(t *testing.T) {
 	e := New()
 	r := e.router
@@ -740,8 +803,8 @@ func TestRouterParamWithSlash(t *testing.T) {
 	r.Find(http.MethodGet, "/a/1/c/d/2/3", c) // `2/3` should mapped to path `/a/:b/c/d/:e` and into `:e`
 
 	err := c.handler(c)
-	assert.Equal(t, nil, c.Get("path"))                      // FIXME: should be "/a/:b/c/d/:e"
-	assert.EqualError(t, err, "code=404, message=Not Found") // FIXME: should be .NoError()
+	assert.Equal(t, "/a/:b/c/d/:e", c.Get("path"))
+	assert.NoError(t, err)
 }
 
 // Issue #1754 - router needs to backtrack multiple levels upwards in tree to find the matching route
@@ -2004,10 +2067,10 @@ func TestRouterParam1466(t *testing.T) {
 			expectRoute: "/users/:username",
 			expectParam: map[string]string{"username": "sharewithme"},
 		},
-		{
+		{ // route `/users/signup` is registered for POST. so param route `/users/:username` (lesser priority) is matched as it has GET handler
 			whenURL:     "/users/signup",
-			expectRoute: nil, // method not found as this route is for POST but request is for GET
-			expectParam: map[string]string{"username": ""},
+			expectRoute: "/users/:username",
+			expectParam: map[string]string{"username": "signup"},
 		},
 		// Additional assertions for #1479
 		{