Repository: labstack/echo
Pull request #2277 (2022-09-19 04:15:14)

Add more http error values
- Add more http error values.


aldas: You can not rename existing public variables - this is not backwards compatible and would break others repos compiling
siyual-park: > * ErrStatusRequestEntityTooLarge

Ok, I rollback the "ErrStatusRequestEntityTooLarge"

Patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
new file mode 100644
index 0000000..d2d3386
--- /dev/null
+++ b/.github/workflows/checks.yml
@@ -0,0 +1,48 @@
+name: Run checks
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+  workflow_dispatch:
+
+permissions:
+  contents: read #  to fetch code (actions/checkout)
+
+env:
+  # run static analysis only with the latest Go version
+  LATEST_GO_VERSION: "1.20"
+
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v3
+
+      - name: Set up Go ${{ matrix.go }}
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ env.LATEST_GO_VERSION }}
+          check-latest: true
+
+      - name: Run golint
+        run: |
+          go install golang.org/x/lint/golint@latest
+          golint -set_exit_status ./...
+
+      - name: Run staticcheck
+        run: |
+          go install honnef.co/go/tools/cmd/staticcheck@latest
+          staticcheck ./...
+
+      - name: Run govulncheck
+        run: |
+          go version
+          go install golang.org/x/vuln/cmd/govulncheck@latest
+          govulncheck ./...
+
+
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index db60f7f..e06183d 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -4,66 +4,53 @@ on:
   push:
     branches:
       - master
-    paths:
-      - '**.go'
-      - 'go.*'
-      - '_fixture/**'
-      - '.github/**'
-      - 'codecov.yml'
   pull_request:
     branches:
       - master
-    paths:
-      - '**.go'
-      - 'go.*'
-      - '_fixture/**'
-      - '.github/**'
-      - 'codecov.yml'
   workflow_dispatch:
 
+permissions:
+  contents: read #  to fetch code (actions/checkout)
+
+env:
+  # run coverage and benchmarks only with the latest Go version
+  LATEST_GO_VERSION: "1.20"
+
 jobs:
   test:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
         # 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.16, 1.17, 1.18, 1.19]
+        # Echo tests with last four major releases (unless there are pressing vulnerabilities)
+        # As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when
+        # we derive from last four major releases promise.
+        go: ["1.18", "1.19", "1.20"]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
       - name: Checkout Code
         uses: actions/checkout@v3
-        with:
-          ref: ${{ github.ref }}
 
       - name: Set up Go ${{ matrix.go }}
         uses: actions/setup-go@v3
         with:
           go-version: ${{ matrix.go }}
 
-      - name: Install Dependencies
-        run: go install golang.org/x/lint/golint@latest
-
       - name: Run Tests
-        run: |
-          golint -set_exit_status ./...
-          go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
+        run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
 
       - name: Upload coverage to Codecov
-        if: success() && matrix.go == 1.19 && matrix.os == 'ubuntu-latest'
+        if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest'
         uses: codecov/codecov-action@v3
         with:
           token:
           fail_ci_if_error: false
+
   benchmark:
     needs: test
-    strategy:
-      matrix:
-        os: [ubuntu-latest]
-        go: [1.19]
-    name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
-    runs-on: ${{ matrix.os }}
+    name: Benchmark comparison
+    runs-on: ubuntu-latest
     steps:
       - name: Checkout Code (Previous)
         uses: actions/checkout@v3
@@ -79,7 +66,7 @@ jobs:
       - name: Set up Go ${{ matrix.go }}
         uses: actions/setup-go@v3
         with:
-          go-version: ${{ matrix.go }}
+          go-version: ${{ env.LATEST_GO_VERSION }}
 
       - name: Install Dependencies
         run: go install golang.org/x/perf/cmd/benchstat@latest
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 67d45ad..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-arch:
-  - amd64
-  - ppc64le
-  
-language: go
-go:
-  - 1.14.x
-  - 1.15.x
-  - tip
-env:
-  - GO111MODULE=on
-install:
-  - go get -v golang.org/x/lint/golint
-script:
-  - golint -set_exit_status ./...
-  - go test -race -coverprofile=coverage.txt -covermode=atomic ./...
-after_success:
-  - bash <(curl -s https://codecov.io/bash)
-matrix:
-  allow_failures:
-    - go: tip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8f4220..28b6f86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,71 @@
 # Changelog
 
+## v4.10.1 - 2023-02-19
+
+**Security**
+
+* Upgrade deps due to the latest golang.org/x/net vulnerability [#2402](https://github.com/labstack/echo/pull/2402)
+
+
+**Enhancements**
+
+* Add new JWT repository to the README [#2377](https://github.com/labstack/echo/pull/2377)
+* Return an empty string for ctx.path if there is no registered path [#2385](https://github.com/labstack/echo/pull/2385)
+* Add context timeout middleware [#2380](https://github.com/labstack/echo/pull/2380)
+* Update link to jaegertracing [#2394](https://github.com/labstack/echo/pull/2394)
+
+
+## v4.10.0 - 2022-12-27
+
+**Security**
+
+* We are deprecating JWT middleware in this repository. Please use https://github.com/labstack/echo-jwt instead. 
+
+  JWT middleware is moved to separate repository to allow us to bump/upgrade version of JWT implementation (`github.com/golang-jwt/jwt`) we are using
+which we can not do in Echo core because this would break backwards compatibility guarantees we try to maintain.
+
+* This minor version bumps minimum Go version to 1.17 (from 1.16) due `golang.org/x/` packages we depend on. There are
+  several vulnerabilities fixed in these libraries.
+
+  Echo still tries to support last 4 Go versions but there are occasions we can not guarantee this promise.
+
+
+**Enhancements**
+
+* Bump x/text to 0.3.8 [#2305](https://github.com/labstack/echo/pull/2305)
+* Bump dependencies and add notes about Go releases we support [#2336](https://github.com/labstack/echo/pull/2336)
+* Add helper interface for ProxyBalancer interface [#2316](https://github.com/labstack/echo/pull/2316)
+* Expose `middleware.CreateExtractors` function so we can use it from echo-contrib repository [#2338](https://github.com/labstack/echo/pull/2338)
+* Refactor func(Context) error to HandlerFunc [#2315](https://github.com/labstack/echo/pull/2315)
+* Improve function comments [#2329](https://github.com/labstack/echo/pull/2329)
+* Add new method HTTPError.WithInternal [#2340](https://github.com/labstack/echo/pull/2340)
+* Replace io/ioutil package usages [#2342](https://github.com/labstack/echo/pull/2342)
+* Add staticcheck to CI flow [#2343](https://github.com/labstack/echo/pull/2343)
+* Replace relative path determination from proprietary to std [#2345](https://github.com/labstack/echo/pull/2345)
+* Remove square brackets from ipv6 addresses in XFF (X-Forwarded-For header) [#2182](https://github.com/labstack/echo/pull/2182)
+* Add testcases for some BodyLimit middleware configuration options [#2350](https://github.com/labstack/echo/pull/2350)
+* Additional configuration options for RequestLogger and Logger middleware [#2341](https://github.com/labstack/echo/pull/2341)
+* Add route to request log [#2162](https://github.com/labstack/echo/pull/2162)
+* GitHub Workflows security hardening [#2358](https://github.com/labstack/echo/pull/2358)
+* Add govulncheck to CI and bump dependencies [#2362](https://github.com/labstack/echo/pull/2362)
+* Fix rate limiter docs [#2366](https://github.com/labstack/echo/pull/2366)
+* Refactor how `e.Routes()` work and introduce `e.OnAddRouteHandler` callback [#2337](https://github.com/labstack/echo/pull/2337)
+
+
+## v4.9.1 - 2022-10-12
+
+**Fixes**
+
+* Fix logger panicing (when template is set to empty) by bumping dependency version [#2295](https://github.com/labstack/echo/issues/2295)
+
+**Enhancements**
+
+* Improve CORS documentation [#2272](https://github.com/labstack/echo/pull/2272)
+* Update readme about supported Go versions [#2291](https://github.com/labstack/echo/pull/2291)
+* Tests: improve error handling on closing body [#2254](https://github.com/labstack/echo/pull/2254)
+* Tests: refactor some of the assertions in tests [#2275](https://github.com/labstack/echo/pull/2275)
+* Tests: refactor assertions [#2301](https://github.com/labstack/echo/pull/2301)
+
 ## v4.9.0 - 2022-09-04
 
 **Security**
diff --git a/Makefile b/Makefile
index a6c4aaa..6aff6a8 100644
--- a/Makefile
+++ b/Makefile
@@ -10,8 +10,10 @@ check: lint vet race ## Check project
 
 init:
 	@go install golang.org/x/lint/golint@latest
+	@go install honnef.co/go/tools/cmd/staticcheck@latest
 
 lint: ## Lint the files
+	@staticcheck ${PKG_LIST}
 	@golint -set_exit_status ${PKG_LIST}
 
 vet: ## Vet the files
@@ -29,6 +31,6 @@ benchmark: ## Run benchmarks
 help: ## Display this help screen
 	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
 
-goversion ?= "1.16"
-test_version: ## Run tests inside Docker with given version (defaults to 1.15 oldest supported). Example: make test_version goversion=1.16
+goversion ?= "1.17"
+test_version: ## Run tests inside Docker with given version (defaults to 1.17 oldest supported). Example: make test_version goversion=1.17
 	@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check"
diff --git a/README.md b/README.md
index 17e6ed9..fe78b6e 100644
--- a/README.md
+++ b/README.md
@@ -11,13 +11,12 @@
 
 ## Supported Go versions
 
+Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with
+older versions.
+
 As of version 4.0.0, Echo is available as a [Go module](https://github.com/golang/go/wiki/Modules).
 Therefore a Go version capable of understanding /vN suffixed imports is required:
 
-- 1.9.7+
-- 1.10.3+
-- 1.14+
-
 Any of these versions will allow you to import Echo as `github.com/labstack/echo/v4` which is the recommended
 way of using Echo going forward.
 
@@ -91,18 +90,29 @@ func hello(c echo.Context) error {
 }
 ```
 
-# Third-party middlewares
-
-| Repository                                                                                           | Description                                                                                                                                                                                                                                                                                                                                  |
-|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib)                         | (by Echo team) [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | 
-| [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen)                                      | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                                                                                                                                                          |
-| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger)                             | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                                                                                                                                                    |
-| [github.com/ziflex/lecho](https://github.com/ziflex/lecho)                                           | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                                  |
-| [github.com/brpaz/echozap](https://github.com/brpaz/echozap)                                         | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                                                                                                                                                              |
-| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs.                                                                                                                                     |
-| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda)                         | Rapid, easy full-stack web development starter kit built with Echo.                                                                                                                                                                                                                                                                          |
-| [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo)                       | ProtoBuf generate Echo server side code                                                                                                                                                                                                                                                                                                      |
+# Official middleware repositories
+
+Following list of middleware is maintained by Echo team.
+
+| Repository                                                                   | Description                                                                                                                                                                                                                                                                                                                   |
+|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt)         | [JWT](https://github.com/golang-jwt/jwt) middleware                                                                                                                                                                                                                                                                           | 
+| [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib) | [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](https://github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | 
+
+# Third-party middleware repositories
+
+Be careful when adding 3rd party middleware. Echo teams does not have time or manpower to guarantee safety and quality
+of middlewares in this list.
+
+| Repository                                                                                           | Description                                                                                                                                                                                              |
+|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen)                                      | Automatically generate RESTful API documentation with [OpenAPI](https://swagger.io/specification/) Client and Server Code Generator                                                                      |
+| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger)                             | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0.                                                                                                                |
+| [github.com/ziflex/lecho](https://github.com/ziflex/lecho)                                           | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface.                                                                                                              |
+| [github.com/brpaz/echozap](https://github.com/brpaz/echozap)                                         | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface.                                                                                                          |
+| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs. |
+| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda)                         | Rapid, easy full-stack web development starter kit built with Echo.                                                                                                                                      |
+| [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo)                       | ProtoBuf generate Echo server side code                                                                                                                                                                  |
 
 Please send a PR to add your own library here.
 
diff --git a/context.go b/context.go
index a4ecfad..b3a7ce8 100644
--- a/context.go
+++ b/context.go
@@ -169,7 +169,11 @@ type (
 		// Redirect redirects the request to a provided URL with status code.
 		Redirect(code int, url string) error
 
-		// Error invokes the registered HTTP error handler. Generally used by middleware.
+		// Error invokes the registered global HTTP error handler. Generally used by middleware.
+		// A side-effect of calling global error handler is that now Response has been committed (sent to the client) and
+		// middlewares up in chain can not change Response status code or Response body anymore.
+		//
+		// Avoid using this method in handlers as no middleware will be able to effectively handle errors after that.
 		Error(err error)
 
 		// Handler returns the matched handler by router.
@@ -181,7 +185,7 @@ type (
 		// Logger returns the `Logger` instance.
 		Logger() Logger
 
-		// Set the logger
+		// SetLogger Set the logger
 		SetLogger(l Logger)
 
 		// Echo returns the `Echo` instance.
@@ -282,11 +286,16 @@ func (c *context) RealIP() string {
 	if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
 		i := strings.IndexAny(ip, ",")
 		if i > 0 {
-			return strings.TrimSpace(ip[:i])
+			xffip := strings.TrimSpace(ip[:i])
+			xffip = strings.TrimPrefix(xffip, "[")
+			xffip = strings.TrimSuffix(xffip, "]")
+			return xffip
 		}
 		return ip
 	}
 	if ip := c.request.Header.Get(HeaderXRealIP); ip != "" {
+		ip = strings.TrimPrefix(ip, "[")
+		ip = strings.TrimSuffix(ip, "]")
 		return ip
 	}
 	ra, _, _ := net.SplitHostPort(c.request.RemoteAddr)
diff --git a/echo.go b/echo.go
index 5ae8a14..ef99c22 100644
--- a/echo.go
+++ b/echo.go
@@ -3,50 +3,49 @@ Package echo implements high performance, minimalist Go web framework.
 
 Example:
 
-  package main
+	package main
 
-  import (
-    "net/http"
+	import (
+	  "net/http"
 
-    "github.com/labstack/echo/v4"
-    "github.com/labstack/echo/v4/middleware"
-  )
+	  "github.com/labstack/echo/v4"
+	  "github.com/labstack/echo/v4/middleware"
+	)
 
-  // Handler
-  func hello(c echo.Context) error {
-    return c.String(http.StatusOK, "Hello, World!")
-  }
+	// Handler
+	func hello(c echo.Context) error {
+	  return c.String(http.StatusOK, "Hello, World!")
+	}
 
-  func main() {
-    // Echo instance
-    e := echo.New()
+	func main() {
+	  // Echo instance
+	  e := echo.New()
 
-    // Middleware
-    e.Use(middleware.Logger())
-    e.Use(middleware.Recover())
+	  // Middleware
+	  e.Use(middleware.Logger())
+	  e.Use(middleware.Recover())
 
-    // Routes
-    e.GET("/", hello)
+	  // Routes
+	  e.GET("/", hello)
 
-    // Start server
-    e.Logger.Fatal(e.Start(":1323"))
-  }
+	  // Start server
+	  e.Logger.Fatal(e.Start(":1323"))
+	}
 
 Learn more at https://echo.labstack.com
 */
 package echo
 
 import (
-	"bytes"
 	stdContext "context"
 	"crypto/tls"
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	stdLog "log"
 	"net"
 	"net/http"
+	"os"
 	"reflect"
 	"runtime"
 	"sync"
@@ -62,20 +61,28 @@ import (
 
 type (
 	// Echo is the top-level framework instance.
+	//
+	// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these
+	// fields from handlers/middlewares and changing field values at the same time leads to data-races.
+	// Adding new routes after the server has been started is also not safe!
 	Echo struct {
 		filesystem
 		common
 		// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
 		// listener address info (on which interface/port was listener binded) without having data races.
-		startupMutex     sync.RWMutex
+		startupMutex sync.RWMutex
+		colorer      *color.Color
+
+		// premiddleware are middlewares that are run before routing is done. In case a pre-middleware returns
+		// an error the router is not executed and the request will end up in the global error handler.
+		premiddleware []MiddlewareFunc
+		middleware    []MiddlewareFunc
+		maxParam      *int
+		router        *Router
+		routers       map[string]*Router
+		pool          sync.Pool
+
 		StdLogger        *stdLog.Logger
-		colorer          *color.Color
-		premiddleware    []MiddlewareFunc
-		middleware       []MiddlewareFunc
-		maxParam         *int
-		router           *Router
-		routers          map[string]*Router
-		pool             sync.Pool
 		Server           *http.Server
 		TLSServer        *http.Server
 		Listener         net.Listener
@@ -93,6 +100,9 @@ type (
 		Logger           Logger
 		IPExtractor      IPExtractor
 		ListenerNetwork  string
+
+		// OnAddRouteHandler is called when Echo adds new route to specific host router.
+		OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc)
 	}
 
 	// Route contains a handler and information for matching against requests.
@@ -116,7 +126,7 @@ type (
 	HandlerFunc func(c Context) error
 
 	// HTTPErrorHandler is a centralized HTTP error handler.
-	HTTPErrorHandler func(error, Context)
+	HTTPErrorHandler func(err error, c Context)
 
 	// Validator is the interface that wraps the Validate function.
 	Validator interface {
@@ -248,7 +258,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.9.0"
+	Version = "4.10.1"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
@@ -281,24 +291,53 @@ var (
 
 // Errors
 var (
-	ErrUnsupportedMediaType        = NewHTTPError(http.StatusUnsupportedMediaType)
-	ErrNotFound                    = NewHTTPError(http.StatusNotFound)
-	ErrUnauthorized                = NewHTTPError(http.StatusUnauthorized)
-	ErrForbidden                   = NewHTTPError(http.StatusForbidden)
-	ErrMethodNotAllowed            = NewHTTPError(http.StatusMethodNotAllowed)
-	ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge)
-	ErrTooManyRequests             = NewHTTPError(http.StatusTooManyRequests)
-	ErrBadRequest                  = NewHTTPError(http.StatusBadRequest)
-	ErrBadGateway                  = NewHTTPError(http.StatusBadGateway)
-	ErrInternalServerError         = NewHTTPError(http.StatusInternalServerError)
-	ErrRequestTimeout              = NewHTTPError(http.StatusRequestTimeout)
-	ErrServiceUnavailable          = NewHTTPError(http.StatusServiceUnavailable)
-	ErrValidatorNotRegistered      = errors.New("validator not registered")
-	ErrRendererNotRegistered       = errors.New("renderer not registered")
-	ErrInvalidRedirectCode         = errors.New("invalid redirect status code")
-	ErrCookieNotFound              = errors.New("cookie not found")
-	ErrInvalidCertOrKeyType        = errors.New("invalid cert or key type, must be string or []byte")
-	ErrInvalidListenerNetwork      = errors.New("invalid listener network")
+	ErrBadRequest                    = NewHTTPError(http.StatusBadRequest)                    // HTTP 400 Bad Request
+	ErrUnauthorized                  = NewHTTPError(http.StatusUnauthorized)                  // HTTP 401 Unauthorized
+	ErrPaymentRequired               = NewHTTPError(http.StatusPaymentRequired)               // HTTP 402 Payment Required
+	ErrForbidden                     = NewHTTPError(http.StatusForbidden)                     // HTTP 403 Forbidden
+	ErrNotFound                      = NewHTTPError(http.StatusNotFound)                      // HTTP 404 Not Found
+	ErrMethodNotAllowed              = NewHTTPError(http.StatusMethodNotAllowed)              // HTTP 405 Method Not Allowed
+	ErrNotAcceptable                 = NewHTTPError(http.StatusNotAcceptable)                 // HTTP 406 Not Acceptable
+	ErrProxyAuthRequired             = NewHTTPError(http.StatusProxyAuthRequired)             // HTTP 407 Proxy AuthRequired
+	ErrRequestTimeout                = NewHTTPError(http.StatusRequestTimeout)                // HTTP 408 Request Timeout
+	ErrConflict                      = NewHTTPError(http.StatusConflict)                      // HTTP 409 Conflict
+	ErrGone                          = NewHTTPError(http.StatusGone)                          // HTTP 410 Gone
+	ErrLengthRequired                = NewHTTPError(http.StatusLengthRequired)                // HTTP 411 Length Required
+	ErrPreconditionFailed            = NewHTTPError(http.StatusPreconditionFailed)            // HTTP 412 Precondition Failed
+	ErrStatusRequestEntityTooLarge   = NewHTTPError(http.StatusRequestEntityTooLarge)         // HTTP 413 Payload Too Large
+	ErrRequestURITooLong             = NewHTTPError(http.StatusRequestURITooLong)             // HTTP 414 URI Too Long
+	ErrUnsupportedMediaType          = NewHTTPError(http.StatusUnsupportedMediaType)          // HTTP 415 Unsupported Media Type
+	ErrRequestedRangeNotSatisfiable  = NewHTTPError(http.StatusRequestedRangeNotSatisfiable)  // HTTP 416 Range Not Satisfiable
+	ErrExpectationFailed             = NewHTTPError(http.StatusExpectationFailed)             // HTTP 417 Expectation Failed
+	ErrTeapot                        = NewHTTPError(http.StatusTeapot)                        // HTTP 418 I'm a teapot
+	ErrMisdirectedRequest            = NewHTTPError(http.StatusMisdirectedRequest)            // HTTP 421 Misdirected Request
+	ErrUnprocessableEntity           = NewHTTPError(http.StatusUnprocessableEntity)           // HTTP 422 Unprocessable Entity
+	ErrLocked                        = NewHTTPError(http.StatusLocked)                        // HTTP 423 Locked
+	ErrFailedDependency              = NewHTTPError(http.StatusFailedDependency)              // HTTP 424 Failed Dependency
+	ErrTooEarly                      = NewHTTPError(http.StatusTooEarly)                      // HTTP 425 Too Early
+	ErrUpgradeRequired               = NewHTTPError(http.StatusUpgradeRequired)               // HTTP 426 Upgrade Required
+	ErrPreconditionRequired          = NewHTTPError(http.StatusPreconditionRequired)          // HTTP 428 Precondition Required
+	ErrTooManyRequests               = NewHTTPError(http.StatusTooManyRequests)               // HTTP 429 Too Many Requests
+	ErrRequestHeaderFieldsTooLarge   = NewHTTPError(http.StatusRequestHeaderFieldsTooLarge)   // HTTP 431 Request Header Fields Too Large
+	ErrUnavailableForLegalReasons    = NewHTTPError(http.StatusUnavailableForLegalReasons)    // HTTP 451 Unavailable For Legal Reasons
+	ErrInternalServerError           = NewHTTPError(http.StatusInternalServerError)           // HTTP 500 Internal Server Error
+	ErrNotImplemented                = NewHTTPError(http.StatusNotImplemented)                // HTTP 501 Not Implemented
+	ErrBadGateway                    = NewHTTPError(http.StatusBadGateway)                    // HTTP 502 Bad Gateway
+	ErrServiceUnavailable            = NewHTTPError(http.StatusServiceUnavailable)            // HTTP 503 Service Unavailable
+	ErrGatewayTimeout                = NewHTTPError(http.StatusGatewayTimeout)                // HTTP 504 Gateway Timeout
+	ErrHTTPVersionNotSupported       = NewHTTPError(http.StatusHTTPVersionNotSupported)       // HTTP 505 HTTP Version Not Supported
+	ErrVariantAlsoNegotiates         = NewHTTPError(http.StatusVariantAlsoNegotiates)         // HTTP 506 Variant Also Negotiates
+	ErrInsufficientStorage           = NewHTTPError(http.StatusInsufficientStorage)           // HTTP 507 Insufficient Storage
+	ErrLoopDetected                  = NewHTTPError(http.StatusLoopDetected)                  // HTTP 508 Loop Detected
+	ErrNotExtended                   = NewHTTPError(http.StatusNotExtended)                   // HTTP 510 Not Extended
+	ErrNetworkAuthenticationRequired = NewHTTPError(http.StatusNetworkAuthenticationRequired) // HTTP 511 Network Authentication Required
+
+	ErrValidatorNotRegistered = errors.New("validator not registered")
+	ErrRendererNotRegistered  = errors.New("renderer not registered")
+	ErrInvalidRedirectCode    = errors.New("invalid redirect status code")
+	ErrCookieNotFound         = errors.New("cookie not found")
+	ErrInvalidCertOrKeyType   = errors.New("invalid cert or key type, must be string or []byte")
+	ErrInvalidListenerNetwork = errors.New("invalid listener network")
 )
 
 // Error handlers
@@ -527,21 +566,20 @@ func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route {
 	return e.file(path, file, e.GET, m...)
 }
 
-func (e *Echo) add(host, method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
-	name := handlerName(handler)
+func (e *Echo) add(host, method, path string, handler HandlerFunc, middlewares ...MiddlewareFunc) *Route {
 	router := e.findRouter(host)
-	// FIXME: when handler+middleware are both nil ... make it behave like handler removal
-	router.Add(method, path, func(c Context) error {
-		h := applyMiddleware(handler, middleware...)
+	//FIXME: when handler+middleware are both nil ... make it behave like handler removal
+	name := handlerName(handler)
+	route := router.add(method, path, name, func(c Context) error {
+		h := applyMiddleware(handler, middlewares...)
 		return h(c)
 	})
-	r := &Route{
-		Method: method,
-		Path:   path,
-		Name:   name,
+
+	if e.OnAddRouteHandler != nil {
+		e.OnAddRouteHandler(host, *route, handler, middlewares)
 	}
-	e.router.routes[method+path] = r
-	return r
+
+	return route
 }
 
 // Add registers a new route for an HTTP method and path with matching handler
@@ -565,7 +603,7 @@ func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
 	return
 }
 
-// URI generates a URI from handler.
+// URI generates an URI from handler.
 func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
 	name := handlerName(handler)
 	return e.Reverse(name, params...)
@@ -578,35 +616,13 @@ func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
 
 // Reverse generates an URL from route name and provided parameters.
 func (e *Echo) Reverse(name string, params ...interface{}) string {
-	uri := new(bytes.Buffer)
-	ln := len(params)
-	n := 0
-	for _, r := range e.router.routes {
-		if r.Name == name {
-			for i, l := 0, len(r.Path); i < l; i++ {
-				if (r.Path[i] == ':' || r.Path[i] == '*') && n < ln {
-					for ; i < l && r.Path[i] != '/'; i++ {
-					}
-					uri.WriteString(fmt.Sprintf("%v", params[n]))
-					n++
-				}
-				if i < l {
-					uri.WriteByte(r.Path[i])
-				}
-			}
-			break
-		}
-	}
-	return uri.String()
+	return e.router.Reverse(name, params...)
 }
 
-// Routes returns the registered routes.
+// Routes returns the registered routes for default router.
+// In case when Echo serves multiple hosts/domains use `e.Routers()["domain2.site"].Routes()` to get specific host routes.
 func (e *Echo) Routes() []*Route {
-	routes := make([]*Route, 0, len(e.router.routes))
-	for _, v := range e.router.routes {
-		routes = append(routes, v)
-	}
-	return routes
+	return e.router.Routes()
 }
 
 // AcquireContext returns an empty `Context` instance from the pool.
@@ -626,7 +642,7 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// Acquire context
 	c := e.pool.Get().(*context)
 	c.Reset(r, w)
-	var h func(Context) error
+	var h HandlerFunc
 
 	if e.premiddleware == nil {
 		e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
@@ -700,7 +716,7 @@ func (e *Echo) StartTLS(address string, certFile, keyFile interface{}) (err erro
 func filepathOrContent(fileOrContent interface{}) (content []byte, err error) {
 	switch v := fileOrContent.(type) {
 	case string:
-		return ioutil.ReadFile(v)
+		return os.ReadFile(v)
 	case []byte:
 		return v, nil
 	default:
@@ -884,6 +900,15 @@ func (he *HTTPError) SetInternal(err error) *HTTPError {
 	return he
 }
 
+// WithInternal returns clone of HTTPError with err set to HTTPError.Internal field
+func (he *HTTPError) WithInternal(err error) *HTTPError {
+	return &HTTPError{
+		Code:     he.Code,
+		Message:  he.Message,
+		Internal: err,
+	}
+}
+
 // Unwrap satisfies the Go 1.13 error wrapper interface.
 func (he *HTTPError) Unwrap() error {
 	return he.Internal
@@ -913,8 +938,8 @@ func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc {
 
 // GetPath returns RawPath, if it's empty returns Path from URL
 // Difference between RawPath and Path is:
-//  * Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
-//  * RawPath is an optional field which only gets set if the default encoding is different from Path.
+//   - Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
+//   - RawPath is an optional field which only gets set if the default encoding is different from Path.
 func GetPath(r *http.Request) string {
 	path := r.URL.RawPath
 	if path == "" {
diff --git a/echo_fs.go b/echo_fs.go
index b8526da..9f83a03 100644
--- a/echo_fs.go
+++ b/echo_fs.go
@@ -7,7 +7,6 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 )
 
@@ -125,7 +124,7 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
 		// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.
 		// fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we
 		// were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs
-		if isRelativePath(root) {
+		if !filepath.IsAbs(root) {
 			root = filepath.Join(dFS.prefix, root)
 		}
 		return &defaultFS{
@@ -136,21 +135,6 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
 	return fs.Sub(currentFs, root)
 }
 
-func isRelativePath(path string) bool {
-	if path == "" {
-		return true
-	}
-	if path[0] == '/' {
-		return false
-	}
-	if runtime.GOOS == "windows" && strings.IndexByte(path, ':') != -1 {
-		// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#file_and_directory_names
-		// https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
-		return false
-	}
-	return true
-}
-
 // MustSubFS creates sub FS from current filesystem or panic on failure.
 // Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.
 //
diff --git a/go.mod b/go.mod
index 4de2bdd..265b0aa 100644
--- a/go.mod
+++ b/go.mod
@@ -4,21 +4,21 @@ go 1.17
 
 require (
 	github.com/golang-jwt/jwt v3.2.2+incompatible
-	github.com/labstack/gommon v0.3.1
-	github.com/stretchr/testify v1.7.0
-	github.com/valyala/fasttemplate v1.2.1
-	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
-	golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f
-	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
+	github.com/labstack/gommon v0.4.0
+	github.com/stretchr/testify v1.8.1
+	github.com/valyala/fasttemplate v1.2.2
+	golang.org/x/crypto v0.6.0
+	golang.org/x/net v0.7.0
+	golang.org/x/time v0.3.0
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/mattn/go-colorable v0.1.11 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
-	golang.org/x/text v0.3.7 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index f667342..79ff318 100644
--- a/go.sum
+++ b/go.sum
@@ -3,43 +3,72 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
-github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
-github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
 github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
 golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
-golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/ip.go b/ip.go
index 46d464c..1bcd756 100644
--- a/ip.go
+++ b/ip.go
@@ -227,6 +227,8 @@ func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
 	return func(req *http.Request) string {
 		realIP := req.Header.Get(HeaderXRealIP)
 		if realIP != "" {
+			realIP = strings.TrimPrefix(realIP, "[")
+			realIP = strings.TrimSuffix(realIP, "]")
 			if ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) {
 				return realIP
 			}
@@ -248,7 +250,10 @@ func ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor {
 		}
 		ips := append(strings.Split(strings.Join(xffs, ","), ","), directIP)
 		for i := len(ips) - 1; i >= 0; i-- {
-			ip := net.ParseIP(strings.TrimSpace(ips[i]))
+			ips[i] = strings.TrimSpace(ips[i])
+			ips[i] = strings.TrimPrefix(ips[i], "[")
+			ips[i] = strings.TrimSuffix(ips[i], "]")
+			ip := net.ParseIP(ips[i])
 			if ip == nil {
 				// Unable to parse IP; cannot trust entire records
 				return directIP
diff --git a/middleware/body_dump.go b/middleware/body_dump.go
index ebd0d0a..fa7891b 100644
--- a/middleware/body_dump.go
+++ b/middleware/body_dump.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"bytes"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 
@@ -68,9 +67,9 @@ func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
 			// Request
 			reqBody := []byte{}
 			if c.Request().Body != nil { // Read
-				reqBody, _ = ioutil.ReadAll(c.Request().Body)
+				reqBody, _ = io.ReadAll(c.Request().Body)
 			}
-			c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
+			c.Request().Body = io.NopCloser(bytes.NewBuffer(reqBody)) // Reset
 
 			// Response
 			resBody := new(bytes.Buffer)
diff --git a/middleware/compress.go b/middleware/compress.go
index ac6672e..9e5f610 100644
--- a/middleware/compress.go
+++ b/middleware/compress.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"compress/gzip"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"strings"
@@ -89,7 +88,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 						// nothing is written to body or error is returned.
 						// See issue #424, #407.
 						res.Writer = rw
-						w.Reset(ioutil.Discard)
+						w.Reset(io.Discard)
 					}
 					w.Close()
 					pool.Put(w)
@@ -135,7 +134,7 @@ func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
 func gzipCompressPool(config GzipConfig) sync.Pool {
 	return sync.Pool{
 		New: func() interface{} {
-			w, err := gzip.NewWriterLevel(ioutil.Discard, config.Level)
+			w, err := gzip.NewWriterLevel(io.Discard, config.Level)
 			if err != nil {
 				return err
 			}
diff --git a/middleware/context_timeout.go b/middleware/context_timeout.go
new file mode 100644
index 0000000..be260e1
--- /dev/null
+++ b/middleware/context_timeout.go
@@ -0,0 +1,72 @@
+package middleware
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/labstack/echo/v4"
+)
+
+// ContextTimeoutConfig defines the config for ContextTimeout middleware.
+type ContextTimeoutConfig struct {
+	// Skipper defines a function to skip middleware.
+	Skipper Skipper
+
+	// ErrorHandler is a function when error aries in middeware execution.
+	ErrorHandler func(err error, c echo.Context) error
+
+	// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
+	Timeout time.Duration
+}
+
+// ContextTimeout returns a middleware which returns error (503 Service Unavailable error) to client
+// when underlying method returns context.DeadlineExceeded error.
+func ContextTimeout(timeout time.Duration) echo.MiddlewareFunc {
+	return ContextTimeoutWithConfig(ContextTimeoutConfig{Timeout: timeout})
+}
+
+// ContextTimeoutWithConfig returns a Timeout middleware with config.
+func ContextTimeoutWithConfig(config ContextTimeoutConfig) echo.MiddlewareFunc {
+	mw, err := config.ToMiddleware()
+	if err != nil {
+		panic(err)
+	}
+	return mw
+}
+
+// ToMiddleware converts Config to middleware.
+func (config ContextTimeoutConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
+	if config.Timeout == 0 {
+		return nil, errors.New("timeout must be set")
+	}
+	if config.Skipper == nil {
+		config.Skipper = DefaultSkipper
+	}
+	if config.ErrorHandler == nil {
+		config.ErrorHandler = func(err error, c echo.Context) error {
+			if err != nil && errors.Is(err, context.DeadlineExceeded) {
+				return echo.ErrServiceUnavailable.WithInternal(err)
+			}
+			return err
+		}
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			timeoutContext, cancel := context.WithTimeout(c.Request().Context(), config.Timeout)
+			defer cancel()
+
+			c.SetRequest(c.Request().WithContext(timeoutContext))
+
+			if err := next(c); err != nil {
+				return config.ErrorHandler(err, c)
+			}
+			return nil
+		}
+	}, nil
+}
diff --git a/middleware/csrf.go b/middleware/csrf.go
index ea90fdb..8661c9f 100644
--- a/middleware/csrf.go
+++ b/middleware/csrf.go
@@ -119,7 +119,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 		config.CookieSecure = true
 	}
 
-	extractors, err := createExtractors(config.TokenLookup, "")
+	extractors, err := CreateExtractors(config.TokenLookup)
 	if err != nil {
 		panic(err)
 	}
diff --git a/middleware/extractor.go b/middleware/extractor.go
index afdfd81..5d9cee6 100644
--- a/middleware/extractor.go
+++ b/middleware/extractor.go
@@ -24,6 +24,26 @@ var errFormExtractorValueMissing = errors.New("missing value in the form")
 // ValuesExtractor defines a function for extracting values (keys/tokens) from the given context.
 type ValuesExtractor func(c echo.Context) ([]string, error)
 
+// CreateExtractors creates ValuesExtractors from given lookups.
+// Lookups is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
+// to extract key from the request.
+// Possible values:
+//   - "header:<name>" or "header:<name>:<cut-prefix>"
+//     `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
+//     value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
+//     want to cut is `<auth-scheme> ` note the space at the end.
+//     In case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove is `Basic `.
+//   - "query:<name>"
+//   - "param:<name>"
+//   - "form:<name>"
+//   - "cookie:<name>"
+//
+// Multiple sources example:
+// - "header:Authorization,header:X-Api-Key"
+func CreateExtractors(lookups string) ([]ValuesExtractor, error) {
+	return createExtractors(lookups, "")
+}
+
 func createExtractors(lookups string, authScheme string) ([]ValuesExtractor, error) {
 	if lookups == "" {
 		return nil, nil
diff --git a/middleware/jwt.go b/middleware/jwt.go
index bec5167..bd62826 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -154,6 +154,8 @@ var (
 //
 // See: https://jwt.io/introduction
 // See `JWTConfig.TokenLookup`
+//
+// Deprecated: Please use https://github.com/labstack/echo-jwt instead
 func JWT(key interface{}) echo.MiddlewareFunc {
 	c := DefaultJWTConfig
 	c.SigningKey = key
@@ -162,6 +164,8 @@ func JWT(key interface{}) echo.MiddlewareFunc {
 
 // JWTWithConfig returns a JWT auth middleware with config.
 // See: `JWT()`.
+//
+// Deprecated: Please use https://github.com/labstack/echo-jwt instead
 func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 	// Defaults
 	if config.Skipper == nil {
@@ -262,7 +266,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 }
 
 func (config *JWTConfig) defaultParseToken(auth string, c echo.Context) (interface{}, error) {
-	token := new(jwt.Token)
+	var token *jwt.Token
 	var err error
 	// Issue #647, #656
 	if _, ok := config.Claims.(jwt.MapClaims); ok {
diff --git a/middleware/logger.go b/middleware/logger.go
index a21df8f..7958d87 100644
--- a/middleware/logger.go
+++ b/middleware/logger.go
@@ -35,6 +35,7 @@ type (
 		// - host
 		// - method
 		// - path
+		// - route
 		// - protocol
 		// - referer
 		// - user_agent
@@ -47,6 +48,7 @@ type (
 		// - header:<NAME>
 		// - query:<NAME>
 		// - form:<NAME>
+		// - custom (see CustomTagFunc field)
 		//
 		// Example "${remote_ip} ${status}"
 		//
@@ -56,6 +58,11 @@ type (
 		// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
 		CustomTimeFormat string `yaml:"custom_time_format"`
 
+		// CustomTagFunc is function called for `${custom}` tag to output user implemented text by writing it to buf.
+		// Make sure that outputted text creates valid JSON string with other logged tags.
+		// Optional.
+		CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error)
+
 		// Output is a writer where logs in JSON format are written.
 		// Optional. Default value os.Stdout.
 		Output io.Writer
@@ -126,6 +133,11 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
 
 			if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
 				switch tag {
+				case "custom":
+					if config.CustomTagFunc == nil {
+						return 0, nil
+					}
+					return config.CustomTagFunc(c, buf)
 				case "time_unix":
 					return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
 				case "time_unix_milli":
@@ -162,6 +174,8 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
 						p = "/"
 					}
 					return buf.WriteString(p)
+				case "route":
+					return buf.WriteString(c.Path())
 				case "protocol":
 					return buf.WriteString(req.Proto)
 				case "referer":
diff --git a/middleware/proxy.go b/middleware/proxy.go
index 6cfd673..d2cd2aa 100644
--- a/middleware/proxy.go
+++ b/middleware/proxy.go
@@ -72,6 +72,11 @@ type (
 		Next(echo.Context) *ProxyTarget
 	}
 
+	// TargetProvider defines an interface that gives the opportunity for balancer to return custom errors when selecting target.
+	TargetProvider interface {
+		NextTarget(echo.Context) (*ProxyTarget, error)
+	}
+
 	commonBalancer struct {
 		targets []*ProxyTarget
 		mutex   sync.RWMutex
@@ -223,6 +228,7 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
 		}
 	}
 
+	provider, isTargetProvider := config.Balancer.(TargetProvider)
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
 		return func(c echo.Context) (err error) {
 			if config.Skipper(c) {
@@ -231,7 +237,16 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
 
 			req := c.Request()
 			res := c.Response()
-			tgt := config.Balancer.Next(c)
+
+			var tgt *ProxyTarget
+			if isTargetProvider {
+				tgt, err = provider.NextTarget(c)
+				if err != nil {
+					return err
+				}
+			} else {
+				tgt = config.Balancer.Next(c)
+			}
 			c.Set(config.ContextKey, tgt)
 
 			if err := rewriteURL(config.RegexRewrite, req); err != nil {
diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go
index be2b348..f7fae83 100644
--- a/middleware/rate_limiter.go
+++ b/middleware/rate_limiter.go
@@ -155,7 +155,7 @@ type (
 	RateLimiterMemoryStore struct {
 		visitors map[string]*Visitor
 		mutex    sync.Mutex
-		rate     rate.Limit //for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
+		rate     rate.Limit // for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
 
 		burst       int
 		expiresIn   time.Duration
@@ -170,15 +170,16 @@ type (
 
 /*
 NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with
-the provided rate (as req/s). The provided rate less than 1 will be treated as zero.
+the provided rate (as req/s).
 for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
 
 Burst and ExpiresIn will be set to default values.
 
+Note that if the provided rate is a float number and Burst is zero, Burst will be treated as the rounded down value of the rate.
+
 Example (with 20 requests/sec):
 
 	limiterStore := middleware.NewRateLimiterMemoryStore(20)
-
 */
 func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore) {
 	return NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
@@ -188,7 +189,7 @@ func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore)
 
 /*
 NewRateLimiterMemoryStoreWithConfig returns an instance of RateLimiterMemoryStore
-with the provided configuration. Rate must be provided. Burst will be set to the value of
+with the provided configuration. Rate must be provided. Burst will be set to the rounded down value of
 the configured rate if not provided or set to 0.
 
 The build-in memory store is usually capable for modest loads. For higher loads other
@@ -225,7 +226,7 @@ func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (s
 // RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore
 type RateLimiterMemoryStoreConfig struct {
 	Rate      rate.Limit    // Rate of requests allowed to pass as req/s. For more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
-	Burst     int           // Burst additionally allows a number of requests to pass when rate limit is reached
+	Burst     int           // Burst is maximum number of requests to pass at the same moment. It additionally allows a number of requests to pass when rate limit is reached.
 	ExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up
 }
 
diff --git a/middleware/request_logger.go b/middleware/request_logger.go
index 7a4d982..b9e3692 100644
--- a/middleware/request_logger.go
+++ b/middleware/request_logger.go
@@ -10,10 +10,16 @@ import (
 
 // Example for `fmt.Printf`
 // 	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogStatus: true,
-//		LogURI:    true,
+//		LogStatus:   true,
+//		LogURI:      true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
 //		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
-//			fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
+//			if v.Error == nil {
+//				fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
+//			} else {
+//				fmt.Printf("REQUEST_ERROR: uri: %v, status: %v, err: %v\n", v.URI, v.Status, v.Error)
+//			}
 //			return nil
 //		},
 //	}))
@@ -21,14 +27,23 @@ import (
 // Example for Zerolog (https://github.com/rs/zerolog)
 // 	logger := zerolog.New(os.Stdout)
 //	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogURI:    true,
-//		LogStatus: true,
+//		LogURI:      true,
+//		LogStatus:   true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
 //		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
-//			logger.Info().
-//				Str("URI", v.URI).
-//				Int("status", v.Status).
-//				Msg("request")
-//
+//			if v.Error == nil {
+//				logger.Info().
+//					Str("URI", v.URI).
+//					Int("status", v.Status).
+//					Msg("request")
+//			} else {
+//				logger.Error().
+//					Err(v.Error).
+//					Str("URI", v.URI).
+//					Int("status", v.Status).
+//					Msg("request error")
+//			}
 //			return nil
 //		},
 //	}))
@@ -36,29 +51,47 @@ import (
 // Example for Zap (https://github.com/uber-go/zap)
 // 	logger, _ := zap.NewProduction()
 //	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogURI:    true,
-//		LogStatus: true,
+//		LogURI:      true,
+//		LogStatus:   true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
 //		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
-//			logger.Info("request",
-//				zap.String("URI", v.URI),
-//				zap.Int("status", v.Status),
-//			)
-//
+//			if v.Error == nil {
+//				logger.Info("request",
+//					zap.String("URI", v.URI),
+//					zap.Int("status", v.Status),
+//				)
+//			} else {
+//				logger.Error("request error",
+//					zap.String("URI", v.URI),
+//					zap.Int("status", v.Status),
+//					zap.Error(v.Error),
+//				)
+//			}
 //			return nil
 //		},
 //	}))
 //
 // Example for Logrus (https://github.com/sirupsen/logrus)
-// 	log := logrus.New()
+//  log := logrus.New()
 //	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogURI:    true,
-//		LogStatus: true,
-//		LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error {
-//			log.WithFields(logrus.Fields{
-//				"URI":   values.URI,
-//				"status": values.Status,
-//			}).Info("request")
-//
+//		LogURI:      true,
+//		LogStatus:   true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
+//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+//			if v.Error == nil {
+//				log.WithFields(logrus.Fields{
+//					"URI":    v.URI,
+//					"status": v.Status,
+//				}).Info("request")
+//			} else {
+//				log.WithFields(logrus.Fields{
+//					"URI":    v.URI,
+//					"status": v.Status,
+//					"error":  v.Error,
+//				}).Error("request error")
+//			}
 //			return nil
 //		},
 //	}))
@@ -74,6 +107,13 @@ type RequestLoggerConfig struct {
 	// Mandatory.
 	LogValuesFunc func(c echo.Context, v RequestLoggerValues) error
 
+	// HandleError instructs logger to call global error handler when next middleware/handler returns an error.
+	// This is useful when you have custom error handler that can decide to use different status codes.
+	//
+	// A side-effect of calling global error handler is that now Response has been committed and sent to the client
+	// and middlewares up in chain can not change Response status code or response body.
+	HandleError bool
+
 	// LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call).
 	LogLatency bool
 	// LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
@@ -217,6 +257,9 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 				config.BeforeNextFunc(c)
 			}
 			err := next(c)
+			if config.HandleError {
+				c.Error(err)
+			}
 
 			v := RequestLoggerValues{
 				StartTime: start,
@@ -264,7 +307,9 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 			}
 			if config.LogStatus {
 				v.Status = res.Status
-				if err != nil {
+				if err != nil && !config.HandleError {
+					//  this block should not be executed in case of HandleError=true as the global error handler will decide
+					//  the status code. In that case status code could be different from what err contains.
 					var httpErr *echo.HTTPError
 					if errors.As(err, &httpErr) {
 						v.Status = httpErr.Code
@@ -310,6 +355,9 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 				return errOnLog
 			}
 
+			// in case of HandleError=true we are returning the error that we already have handled with global error handler
+			// this is deliberate as this error could be useful for upstream middlewares and default global error handler
+			// will ignore that error when it bubbles up in middleware chain.
 			return err
 		}
 	}, nil
diff --git a/middleware/slash.go b/middleware/slash.go
index 4188675..a3bf807 100644
--- a/middleware/slash.go
+++ b/middleware/slash.go
@@ -33,7 +33,7 @@ func AddTrailingSlash() echo.MiddlewareFunc {
 	return AddTrailingSlashWithConfig(DefaultTrailingSlashConfig)
 }
 
-// AddTrailingSlashWithConfig returns a AddTrailingSlash middleware with config.
+// AddTrailingSlashWithConfig returns an AddTrailingSlash middleware with config.
 // See `AddTrailingSlash()`.
 func AddTrailingSlashWithConfig(config TrailingSlashConfig) echo.MiddlewareFunc {
 	// Defaults
diff --git a/router.go b/router.go
index 23c5bd3..597660d 100644
--- a/router.go
+++ b/router.go
@@ -2,6 +2,7 @@ package echo
 
 import (
 	"bytes"
+	"fmt"
 	"net/http"
 )
 
@@ -141,6 +142,51 @@ func NewRouter(e *Echo) *Router {
 	}
 }
 
+// Routes returns the registered routes.
+func (r *Router) Routes() []*Route {
+	routes := make([]*Route, 0, len(r.routes))
+	for _, v := range r.routes {
+		routes = append(routes, v)
+	}
+	return routes
+}
+
+// Reverse generates an URL from route name and provided parameters.
+func (r *Router) Reverse(name string, params ...interface{}) string {
+	uri := new(bytes.Buffer)
+	ln := len(params)
+	n := 0
+	for _, route := range r.routes {
+		if route.Name == name {
+			for i, l := 0, len(route.Path); i < l; i++ {
+				if (route.Path[i] == ':' || route.Path[i] == '*') && n < ln {
+					for ; i < l && route.Path[i] != '/'; i++ {
+					}
+					uri.WriteString(fmt.Sprintf("%v", params[n]))
+					n++
+				}
+				if i < l {
+					uri.WriteByte(route.Path[i])
+				}
+			}
+			break
+		}
+	}
+	return uri.String()
+}
+
+func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
+	r.Add(method, path, h)
+
+	route := &Route{
+		Method: method,
+		Path:   path,
+		Name:   name,
+	}
+	r.routes[method+path] = route
+	return route
+}
+
 // Add registers a new route for method and path with matching handler.
 func (r *Router) Add(method, path string, h HandlerFunc) {
 	// Validate path
@@ -478,7 +524,6 @@ func optionsMethodHandler(allowMethods string) func(c Context) error {
 // - Return it `Echo#ReleaseContext()`.
 func (r *Router) Find(method, path string, c Context) {
 	ctx := c.(*context)
-	ctx.path = path
 	currentNode := r.tree // Current node as root
 
 	var (


Test patch:
diff --git a/bind_test.go b/bind_test.go
index 8220081..c35283d 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -190,44 +190,40 @@ func TestToMultipleFields(t *testing.T) {
 }
 
 func TestBindJSON(t *testing.T) {
-	assert := assert.New(t)
-	testBindOkay(assert, strings.NewReader(userJSON), nil, MIMEApplicationJSON)
-	testBindOkay(assert, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON)
-	testBindArrayOkay(assert, strings.NewReader(usersJSON), nil, MIMEApplicationJSON)
-	testBindArrayOkay(assert, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON)
-	testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
-	testBindError(assert, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{})
+	testBindOkay(t, strings.NewReader(userJSON), nil, MIMEApplicationJSON)
+	testBindOkay(t, strings.NewReader(userJSON), dummyQuery, MIMEApplicationJSON)
+	testBindArrayOkay(t, strings.NewReader(usersJSON), nil, MIMEApplicationJSON)
+	testBindArrayOkay(t, strings.NewReader(usersJSON), dummyQuery, MIMEApplicationJSON)
+	testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
+	testBindError(t, strings.NewReader(userJSONInvalidType), MIMEApplicationJSON, &json.UnmarshalTypeError{})
 }
 
 func TestBindXML(t *testing.T) {
-	assert := assert.New(t)
-
-	testBindOkay(assert, strings.NewReader(userXML), nil, MIMEApplicationXML)
-	testBindOkay(assert, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
-	testBindArrayOkay(assert, strings.NewReader(userXML), nil, MIMEApplicationXML)
-	testBindArrayOkay(assert, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
-	testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New(""))
-	testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{})
-	testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{})
-	testBindOkay(assert, strings.NewReader(userXML), nil, MIMETextXML)
-	testBindOkay(assert, strings.NewReader(userXML), dummyQuery, MIMETextXML)
-	testBindError(assert, strings.NewReader(invalidContent), MIMETextXML, errors.New(""))
-	testBindError(assert, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{})
-	testBindError(assert, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{})
+	testBindOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)
+	testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
+	testBindArrayOkay(t, strings.NewReader(userXML), nil, MIMEApplicationXML)
+	testBindArrayOkay(t, strings.NewReader(userXML), dummyQuery, MIMEApplicationXML)
+	testBindError(t, strings.NewReader(invalidContent), MIMEApplicationXML, errors.New(""))
+	testBindError(t, strings.NewReader(userXMLConvertNumberError), MIMEApplicationXML, &strconv.NumError{})
+	testBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMEApplicationXML, &xml.SyntaxError{})
+	testBindOkay(t, strings.NewReader(userXML), nil, MIMETextXML)
+	testBindOkay(t, strings.NewReader(userXML), dummyQuery, MIMETextXML)
+	testBindError(t, strings.NewReader(invalidContent), MIMETextXML, errors.New(""))
+	testBindError(t, strings.NewReader(userXMLConvertNumberError), MIMETextXML, &strconv.NumError{})
+	testBindError(t, strings.NewReader(userXMLUnsupportedTypeError), MIMETextXML, &xml.SyntaxError{})
 }
 
 func TestBindForm(t *testing.T) {
-	assert := assert.New(t)
 
-	testBindOkay(assert, strings.NewReader(userForm), nil, MIMEApplicationForm)
-	testBindOkay(assert, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm)
+	testBindOkay(t, strings.NewReader(userForm), nil, MIMEApplicationForm)
+	testBindOkay(t, strings.NewReader(userForm), dummyQuery, MIMEApplicationForm)
 	e := New()
 	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userForm))
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	req.Header.Set(HeaderContentType, MIMEApplicationForm)
 	err := c.Bind(&[]struct{ Field string }{})
-	assert.Error(err)
+	assert.Error(t, err)
 }
 
 func TestBindQueryParams(t *testing.T) {
@@ -317,14 +313,13 @@ func TestBindUnmarshalParam(t *testing.T) {
 	err := c.Bind(&result)
 	ts := Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC))
 
-	assert := assert.New(t)
-	if assert.NoError(err) {
+	if assert.NoError(t, err) {
 		//		assert.Equal( Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T)
-		assert.Equal(ts, result.T)
-		assert.Equal(StringArray([]string{"one", "two", "three"}), result.SA)
-		assert.Equal([]Timestamp{ts, ts}, result.TA)
-		assert.Equal(Struct{""}, result.ST)       // child struct does not have a field with matching tag
-		assert.Equal("baz", result.StWithTag.Foo) // child struct has field with matching tag
+		assert.Equal(t, ts, result.T)
+		assert.Equal(t, StringArray([]string{"one", "two", "three"}), result.SA)
+		assert.Equal(t, []Timestamp{ts, ts}, result.TA)
+		assert.Equal(t, Struct{""}, result.ST)       // child struct does not have a field with matching tag
+		assert.Equal(t, "baz", result.StWithTag.Foo) // child struct has field with matching tag
 	}
 }
 
@@ -426,38 +421,35 @@ func TestBindMultipartForm(t *testing.T) {
 	mw.Close()
 	body := bodyBuffer.Bytes()
 
-	assert := assert.New(t)
-	testBindOkay(assert, bytes.NewReader(body), nil, mw.FormDataContentType())
-	testBindOkay(assert, bytes.NewReader(body), dummyQuery, mw.FormDataContentType())
+	testBindOkay(t, bytes.NewReader(body), nil, mw.FormDataContentType())
+	testBindOkay(t, bytes.NewReader(body), dummyQuery, mw.FormDataContentType())
 }
 
 func TestBindUnsupportedMediaType(t *testing.T) {
-	assert := assert.New(t)
-	testBindError(assert, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
+	testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
 }
 
 func TestBindbindData(t *testing.T) {
-	a := assert.New(t)
 	ts := new(bindTestStruct)
 	b := new(DefaultBinder)
 	err := b.bindData(ts, values, "form")
-	a.NoError(err)
-
-	a.Equal(0, ts.I)
-	a.Equal(int8(0), ts.I8)
-	a.Equal(int16(0), ts.I16)
-	a.Equal(int32(0), ts.I32)
-	a.Equal(int64(0), ts.I64)
-	a.Equal(uint(0), ts.UI)
-	a.Equal(uint8(0), ts.UI8)
-	a.Equal(uint16(0), ts.UI16)
-	a.Equal(uint32(0), ts.UI32)
-	a.Equal(uint64(0), ts.UI64)
-	a.Equal(false, ts.B)
-	a.Equal(float32(0), ts.F32)
-	a.Equal(float64(0), ts.F64)
-	a.Equal("", ts.S)
-	a.Equal("", ts.cantSet)
+	assert.NoError(t, err)
+
+	assert.Equal(t, 0, ts.I)
+	assert.Equal(t, int8(0), ts.I8)
+	assert.Equal(t, int16(0), ts.I16)
+	assert.Equal(t, int32(0), ts.I32)
+	assert.Equal(t, int64(0), ts.I64)
+	assert.Equal(t, uint(0), ts.UI)
+	assert.Equal(t, uint8(0), ts.UI8)
+	assert.Equal(t, uint16(0), ts.UI16)
+	assert.Equal(t, uint32(0), ts.UI32)
+	assert.Equal(t, uint64(0), ts.UI64)
+	assert.Equal(t, false, ts.B)
+	assert.Equal(t, float32(0), ts.F32)
+	assert.Equal(t, float64(0), ts.F64)
+	assert.Equal(t, "", ts.S)
+	assert.Equal(t, "", ts.cantSet)
 }
 
 func TestBindParam(t *testing.T) {
@@ -528,7 +520,6 @@ func TestBindUnmarshalTypeError(t *testing.T) {
 }
 
 func TestBindSetWithProperType(t *testing.T) {
-	assert := assert.New(t)
 	ts := new(bindTestStruct)
 	typ := reflect.TypeOf(ts).Elem()
 	val := reflect.ValueOf(ts).Elem()
@@ -543,9 +534,9 @@ func TestBindSetWithProperType(t *testing.T) {
 		}
 		val := values[typeField.Name][0]
 		err := setWithProperType(typeField.Type.Kind(), val, structField)
-		assert.NoError(err)
+		assert.NoError(t, err)
 	}
-	assertBindTestStruct(assert, ts)
+	assertBindTestStruct(t, ts)
 
 	type foo struct {
 		Bar bytes.Buffer
@@ -553,56 +544,54 @@ func TestBindSetWithProperType(t *testing.T) {
 	v := &foo{}
 	typ = reflect.TypeOf(v).Elem()
 	val = reflect.ValueOf(v).Elem()
-	assert.Error(setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0)))
+	assert.Error(t, setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0)))
 }
 
 func TestBindSetFields(t *testing.T) {
-	assert := assert.New(t)
 
 	ts := new(bindTestStruct)
 	val := reflect.ValueOf(ts).Elem()
 	// Int
-	if assert.NoError(setIntField("5", 0, val.FieldByName("I"))) {
-		assert.Equal(5, ts.I)
+	if assert.NoError(t, setIntField("5", 0, val.FieldByName("I"))) {
+		assert.Equal(t, 5, ts.I)
 	}
-	if assert.NoError(setIntField("", 0, val.FieldByName("I"))) {
-		assert.Equal(0, ts.I)
+	if assert.NoError(t, setIntField("", 0, val.FieldByName("I"))) {
+		assert.Equal(t, 0, ts.I)
 	}
 
 	// Uint
-	if assert.NoError(setUintField("10", 0, val.FieldByName("UI"))) {
-		assert.Equal(uint(10), ts.UI)
+	if assert.NoError(t, setUintField("10", 0, val.FieldByName("UI"))) {
+		assert.Equal(t, uint(10), ts.UI)
 	}
-	if assert.NoError(setUintField("", 0, val.FieldByName("UI"))) {
-		assert.Equal(uint(0), ts.UI)
+	if assert.NoError(t, setUintField("", 0, val.FieldByName("UI"))) {
+		assert.Equal(t, uint(0), ts.UI)
 	}
 
 	// Float
-	if assert.NoError(setFloatField("15.5", 0, val.FieldByName("F32"))) {
-		assert.Equal(float32(15.5), ts.F32)
+	if assert.NoError(t, setFloatField("15.5", 0, val.FieldByName("F32"))) {
+		assert.Equal(t, float32(15.5), ts.F32)
 	}
-	if assert.NoError(setFloatField("", 0, val.FieldByName("F32"))) {
-		assert.Equal(float32(0.0), ts.F32)
+	if assert.NoError(t, setFloatField("", 0, val.FieldByName("F32"))) {
+		assert.Equal(t, float32(0.0), ts.F32)
 	}
 
 	// Bool
-	if assert.NoError(setBoolField("true", val.FieldByName("B"))) {
-		assert.Equal(true, ts.B)
+	if assert.NoError(t, setBoolField("true", val.FieldByName("B"))) {
+		assert.Equal(t, true, ts.B)
 	}
-	if assert.NoError(setBoolField("", val.FieldByName("B"))) {
-		assert.Equal(false, ts.B)
+	if assert.NoError(t, setBoolField("", val.FieldByName("B"))) {
+		assert.Equal(t, false, ts.B)
 	}
 
 	ok, err := unmarshalFieldNonPtr("2016-12-06T19:09:05Z", val.FieldByName("T"))
-	if assert.NoError(err) {
-		assert.Equal(ok, true)
-		assert.Equal(Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), ts.T)
+	if assert.NoError(t, err) {
+		assert.Equal(t, ok, true)
+		assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), ts.T)
 	}
 }
 
 func BenchmarkBindbindDataWithTags(b *testing.B) {
 	b.ReportAllocs()
-	assert := assert.New(b)
 	ts := new(bindTestStructWithTags)
 	binder := new(DefaultBinder)
 	var err error
@@ -610,29 +599,29 @@ func BenchmarkBindbindDataWithTags(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		err = binder.bindData(ts, values, "form")
 	}
-	assert.NoError(err)
-	assertBindTestStruct(assert, (*bindTestStruct)(ts))
+	assert.NoError(b, err)
+	assertBindTestStruct(b, (*bindTestStruct)(ts))
 }
 
-func assertBindTestStruct(a *assert.Assertions, ts *bindTestStruct) {
-	a.Equal(0, ts.I)
-	a.Equal(int8(8), ts.I8)
-	a.Equal(int16(16), ts.I16)
-	a.Equal(int32(32), ts.I32)
-	a.Equal(int64(64), ts.I64)
-	a.Equal(uint(0), ts.UI)
-	a.Equal(uint8(8), ts.UI8)
-	a.Equal(uint16(16), ts.UI16)
-	a.Equal(uint32(32), ts.UI32)
-	a.Equal(uint64(64), ts.UI64)
-	a.Equal(true, ts.B)
-	a.Equal(float32(32.5), ts.F32)
-	a.Equal(float64(64.5), ts.F64)
-	a.Equal("test", ts.S)
-	a.Equal("", ts.GetCantSet())
+func assertBindTestStruct(tb testing.TB, ts *bindTestStruct) {
+	assert.Equal(tb, 0, ts.I)
+	assert.Equal(tb, int8(8), ts.I8)
+	assert.Equal(tb, int16(16), ts.I16)
+	assert.Equal(tb, int32(32), ts.I32)
+	assert.Equal(tb, int64(64), ts.I64)
+	assert.Equal(tb, uint(0), ts.UI)
+	assert.Equal(tb, uint8(8), ts.UI8)
+	assert.Equal(tb, uint16(16), ts.UI16)
+	assert.Equal(tb, uint32(32), ts.UI32)
+	assert.Equal(tb, uint64(64), ts.UI64)
+	assert.Equal(tb, true, ts.B)
+	assert.Equal(tb, float32(32.5), ts.F32)
+	assert.Equal(tb, float64(64.5), ts.F64)
+	assert.Equal(tb, "test", ts.S)
+	assert.Equal(tb, "", ts.GetCantSet())
 }
 
-func testBindOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctype string) {
+func testBindOkay(t *testing.T, r io.Reader, query url.Values, ctype string) {
 	e := New()
 	path := "/"
 	if len(query) > 0 {
@@ -644,13 +633,13 @@ func testBindOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctyp
 	req.Header.Set(HeaderContentType, ctype)
 	u := new(user)
 	err := c.Bind(u)
-	if assert.NoError(err) {
-		assert.Equal(1, u.ID)
-		assert.Equal("Jon Snow", u.Name)
+	if assert.Equal(t, nil, err) {
+		assert.Equal(t, 1, u.ID)
+		assert.Equal(t, "Jon Snow", u.Name)
 	}
 }
 
-func testBindArrayOkay(assert *assert.Assertions, r io.Reader, query url.Values, ctype string) {
+func testBindArrayOkay(t *testing.T, r io.Reader, query url.Values, ctype string) {
 	e := New()
 	path := "/"
 	if len(query) > 0 {
@@ -662,14 +651,14 @@ func testBindArrayOkay(assert *assert.Assertions, r io.Reader, query url.Values,
 	req.Header.Set(HeaderContentType, ctype)
 	u := []user{}
 	err := c.Bind(&u)
-	if assert.NoError(err) {
-		assert.Equal(1, len(u))
-		assert.Equal(1, u[0].ID)
-		assert.Equal("Jon Snow", u[0].Name)
+	if assert.NoError(t, err) {
+		assert.Equal(t, 1, len(u))
+		assert.Equal(t, 1, u[0].ID)
+		assert.Equal(t, "Jon Snow", u[0].Name)
 	}
 }
 
-func testBindError(assert *assert.Assertions, r io.Reader, ctype string, expectedInternal error) {
+func testBindError(t *testing.T, r io.Reader, ctype string, expectedInternal error) {
 	e := New()
 	req := httptest.NewRequest(http.MethodPost, "/", r)
 	rec := httptest.NewRecorder()
@@ -681,14 +670,14 @@ func testBindError(assert *assert.Assertions, r io.Reader, ctype string, expecte
 	switch {
 	case strings.HasPrefix(ctype, MIMEApplicationJSON), strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML),
 		strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
-		if assert.IsType(new(HTTPError), err) {
-			assert.Equal(http.StatusBadRequest, err.(*HTTPError).Code)
-			assert.IsType(expectedInternal, err.(*HTTPError).Internal)
+		if assert.IsType(t, new(HTTPError), err) {
+			assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).Code)
+			assert.IsType(t, expectedInternal, err.(*HTTPError).Internal)
 		}
 	default:
-		if assert.IsType(new(HTTPError), err) {
-			assert.Equal(ErrUnsupportedMediaType, err)
-			assert.IsType(expectedInternal, err.(*HTTPError).Internal)
+		if assert.IsType(t, new(HTTPError), err) {
+			assert.Equal(t, ErrUnsupportedMediaType, err)
+			assert.IsType(t, expectedInternal, err.(*HTTPError).Internal)
 		}
 	}
 }
diff --git a/context_test.go b/context_test.go
index b25e11c..11a63cf 100644
--- a/context_test.go
+++ b/context_test.go
@@ -97,7 +97,6 @@ func (responseWriterErr) Write([]byte) (int, error) {
 }
 
 func (responseWriterErr) WriteHeader(statusCode int) {
-
 }
 
 func TestContext(t *testing.T) {
@@ -904,6 +903,30 @@ func TestContext_RealIP(t *testing.T) {
 			},
 			"127.0.0.1",
 		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"[2001:db8:85a3:8d3:1319:8a2e:370:7348], 2001:db8::1, "}},
+				},
+			},
+			"2001:db8:85a3:8d3:1319:8a2e:370:7348",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"[2001:db8:85a3:8d3:1319:8a2e:370:7348],[2001:db8::1]"}},
+				},
+			},
+			"2001:db8:85a3:8d3:1319:8a2e:370:7348",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"2001:db8:85a3:8d3:1319:8a2e:370:7348"}},
+				},
+			},
+			"2001:db8:85a3:8d3:1319:8a2e:370:7348",
+		},
 		{
 			&context{
 				request: &http.Request{
@@ -914,6 +937,17 @@ func TestContext_RealIP(t *testing.T) {
 			},
 			"192.168.0.1",
 		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{
+						"X-Real-Ip": []string{"[2001:db8::1]"},
+					},
+				},
+			},
+			"2001:db8::1",
+		},
+
 		{
 			&context{
 				request: &http.Request{
diff --git a/echo_test.go b/echo_test.go
index 7fd77c8..2f66c8c 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -244,7 +243,7 @@ func TestEchoStaticRedirectIndex(t *testing.T) {
 		}(resp.Body)
 		assert.Equal(t, http.StatusOK, resp.StatusCode)
 
-		if body, err := ioutil.ReadAll(resp.Body); err == nil {
+		if body, err := io.ReadAll(resp.Body); err == nil {
 			assert.Equal(t, true, strings.HasPrefix(string(body), "<!doctype html>"))
 		} else {
 			assert.Fail(t, err.Error())
@@ -491,16 +490,14 @@ func TestEchoURL(t *testing.T) {
 	g := e.Group("/group")
 	g.GET("/users/:uid/files/:fid", getFile)
 
-	assertion := assert.New(t)
-
-	assertion.Equal("/static/file", e.URL(static))
-	assertion.Equal("/users/:id", e.URL(getUser))
-	assertion.Equal("/users/1", e.URL(getUser, "1"))
-	assertion.Equal("/users/1", e.URL(getUser, "1"))
-	assertion.Equal("/documents/foo.txt", e.URL(getAny, "foo.txt"))
-	assertion.Equal("/documents/*", e.URL(getAny))
-	assertion.Equal("/group/users/1/files/:fid", e.URL(getFile, "1"))
-	assertion.Equal("/group/users/1/files/1", e.URL(getFile, "1", "1"))
+	assert.Equal(t, "/static/file", e.URL(static))
+	assert.Equal(t, "/users/:id", e.URL(getUser))
+	assert.Equal(t, "/users/1", e.URL(getUser, "1"))
+	assert.Equal(t, "/users/1", e.URL(getUser, "1"))
+	assert.Equal(t, "/documents/foo.txt", e.URL(getAny, "foo.txt"))
+	assert.Equal(t, "/documents/*", e.URL(getAny))
+	assert.Equal(t, "/group/users/1/files/:fid", e.URL(getFile, "1"))
+	assert.Equal(t, "/group/users/1/files/1", e.URL(getFile, "1", "1"))
 }
 
 func TestEchoRoutes(t *testing.T) {
@@ -533,9 +530,9 @@ func TestEchoRoutes(t *testing.T) {
 	}
 }
 
-func TestEchoRoutesHandleHostsProperly(t *testing.T) {
+func TestEchoRoutesHandleAdditionalHosts(t *testing.T) {
 	e := New()
-	h := e.Host("route.com")
+	domain2Router := e.Host("domain2.router.com")
 	routes := []*Route{
 		{http.MethodGet, "/users/:user/events", ""},
 		{http.MethodGet, "/users/:user/events/public", ""},
@@ -543,24 +540,61 @@ func TestEchoRoutesHandleHostsProperly(t *testing.T) {
 		{http.MethodPost, "/repos/:owner/:repo/git/tags", ""},
 	}
 	for _, r := range routes {
-		h.Add(r.Method, r.Path, func(c Context) error {
+		domain2Router.Add(r.Method, r.Path, func(c Context) error {
 			return c.String(http.StatusOK, "OK")
 		})
 	}
+	e.Add(http.MethodGet, "/api", func(c Context) error {
+		return c.String(http.StatusOK, "OK")
+	})
 
-	if assert.Equal(t, len(routes), len(e.Routes())) {
-		for _, r := range e.Routes() {
-			found := false
-			for _, rr := range routes {
-				if r.Method == rr.Method && r.Path == rr.Path {
-					found = true
-					break
-				}
+	domain2Routes := e.Routers()["domain2.router.com"].Routes()
+
+	assert.Len(t, domain2Routes, len(routes))
+	for _, r := range domain2Routes {
+		found := false
+		for _, rr := range routes {
+			if r.Method == rr.Method && r.Path == rr.Path {
+				found = true
+				break
 			}
-			if !found {
-				t.Errorf("Route %s %s not found", r.Method, r.Path)
+		}
+		if !found {
+			t.Errorf("Route %s %s not found", r.Method, r.Path)
+		}
+	}
+}
+
+func TestEchoRoutesHandleDefaultHost(t *testing.T) {
+	e := New()
+	routes := []*Route{
+		{http.MethodGet, "/users/:user/events", ""},
+		{http.MethodGet, "/users/:user/events/public", ""},
+		{http.MethodPost, "/repos/:owner/:repo/git/refs", ""},
+		{http.MethodPost, "/repos/:owner/:repo/git/tags", ""},
+	}
+	for _, r := range routes {
+		e.Add(r.Method, r.Path, func(c Context) error {
+			return c.String(http.StatusOK, "OK")
+		})
+	}
+	e.Host("subdomain.mysite.site").Add(http.MethodGet, "/api", func(c Context) error {
+		return c.String(http.StatusOK, "OK")
+	})
+
+	defaultRouterRoutes := e.Routes()
+	assert.Len(t, defaultRouterRoutes, len(routes))
+	for _, r := range defaultRouterRoutes {
+		found := false
+		for _, rr := range routes {
+			if r.Method == rr.Method && r.Path == rr.Path {
+				found = true
+				break
 			}
 		}
+		if !found {
+			t.Errorf("Route %s %s not found", r.Method, r.Path)
+		}
 	}
 }
 
@@ -607,8 +641,6 @@ func TestEchoServeHTTPPathEncoding(t *testing.T) {
 }
 
 func TestEchoHost(t *testing.T) {
-	assertion := assert.New(t)
-
 	okHandler := func(c Context) error { return c.String(http.StatusOK, http.StatusText(http.StatusOK)) }
 	teapotHandler := func(c Context) error { return c.String(http.StatusTeapot, http.StatusText(http.StatusTeapot)) }
 	acceptHandler := func(c Context) error { return c.String(http.StatusAccepted, http.StatusText(http.StatusAccepted)) }
@@ -703,8 +735,8 @@ func TestEchoHost(t *testing.T) {
 
 			e.ServeHTTP(rec, req)
 
-			assertion.Equal(tc.expectStatus, rec.Code)
-			assertion.Equal(tc.expectBody, rec.Body.String())
+			assert.Equal(t, tc.expectStatus, rec.Code)
+			assert.Equal(t, tc.expectBody, rec.Body.String())
 		})
 	}
 }
@@ -1036,9 +1068,9 @@ func TestEchoStartTLSAndStart(t *testing.T) {
 }
 
 func TestEchoStartTLSByteString(t *testing.T) {
-	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	cert, err := os.ReadFile("_fixture/certs/cert.pem")
 	require.NoError(t, err)
-	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	key, err := os.ReadFile("_fixture/certs/key.pem")
 	require.NoError(t, err)
 
 	testCases := []struct {
@@ -1210,13 +1242,22 @@ func TestHTTPError(t *testing.T) {
 
 		assert.Equal(t, "code=400, message=map[code:12]", err.Error())
 	})
-	t.Run("internal", func(t *testing.T) {
+
+	t.Run("internal and SetInternal", func(t *testing.T) {
 		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
 			"code": 12,
 		})
 		err.SetInternal(errors.New("internal error"))
 		assert.Equal(t, "code=400, message=map[code:12], internal=internal error", err.Error())
 	})
+
+	t.Run("internal and WithInternal", func(t *testing.T) {
+		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
+			"code": 12,
+		})
+		err = err.WithInternal(errors.New("internal error"))
+		assert.Equal(t, "code=400, message=map[code:12], internal=internal error", err.Error())
+	})
 }
 
 func TestHTTPError_Unwrap(t *testing.T) {
@@ -1227,13 +1268,22 @@ func TestHTTPError_Unwrap(t *testing.T) {
 
 		assert.Nil(t, errors.Unwrap(err))
 	})
-	t.Run("internal", func(t *testing.T) {
+
+	t.Run("unwrap internal and SetInternal", 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())
 	})
+
+	t.Run("unwrap internal and WithInternal", func(t *testing.T) {
+		err := NewHTTPError(http.StatusBadRequest, map[string]interface{}{
+			"code": 12,
+		})
+		err = err.WithInternal(errors.New("internal error"))
+		assert.Equal(t, "internal error", errors.Unwrap(err).Error())
+	})
 }
 
 func TestDefaultHTTPErrorHandler(t *testing.T) {
@@ -1399,7 +1449,7 @@ func TestEchoListenerNetwork(t *testing.T) {
 				}(resp.Body)
 				assert.Equal(t, http.StatusOK, resp.StatusCode)
 
-				if body, err := ioutil.ReadAll(resp.Body); err == nil {
+				if body, err := io.ReadAll(resp.Body); err == nil {
 					assert.Equal(t, "OK", string(body))
 				} else {
 					assert.Fail(t, err.Error())
@@ -1428,9 +1478,45 @@ func TestEchoListenerNetworkInvalid(t *testing.T) {
 	assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
 }
 
-func TestEchoReverse(t *testing.T) {
-	assert := assert.New(t)
+func TestEcho_OnAddRouteHandler(t *testing.T) {
+	type rr struct {
+		host       string
+		route      Route
+		handler    HandlerFunc
+		middleware []MiddlewareFunc
+	}
+	dummyHandler := func(Context) error { return nil }
+	e := New()
 
+	added := make([]rr, 0)
+	e.OnAddRouteHandler = func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc) {
+		added = append(added, rr{
+			host:       host,
+			route:      route,
+			handler:    handler,
+			middleware: middleware,
+		})
+	}
+
+	e.GET("/static", NotFoundHandler)
+	e.Host("domain.site").GET("/static/*", dummyHandler, func(next HandlerFunc) HandlerFunc {
+		return func(c Context) error {
+			return next(c)
+		}
+	})
+
+	assert.Len(t, added, 2)
+
+	assert.Equal(t, "", added[0].host)
+	assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.glob..func1"}, added[0].route)
+	assert.Len(t, added[0].middleware, 0)
+
+	assert.Equal(t, "domain.site", added[1].host)
+	assert.Equal(t, Route{Method: http.MethodGet, Path: "/static/*", Name: "github.com/labstack/echo/v4.TestEcho_OnAddRouteHandler.func1"}, added[1].route)
+	assert.Len(t, added[1].middleware, 1)
+}
+
+func TestEchoReverse(t *testing.T) {
 	e := New()
 	dummyHandler := func(Context) error { return nil }
 
@@ -1440,33 +1526,44 @@ func TestEchoReverse(t *testing.T) {
 	e.GET("/params/:foo/bar/:qux", dummyHandler).Name = "/params/:foo/bar/:qux"
 	e.GET("/params/:foo/bar/:qux/*", dummyHandler).Name = "/params/:foo/bar/:qux/*"
 
-	assert.Equal("/static", e.Reverse("/static"))
-	assert.Equal("/static", e.Reverse("/static", "missing param"))
-	assert.Equal("/static/*", e.Reverse("/static/*"))
-	assert.Equal("/static/foo.txt", e.Reverse("/static/*", "foo.txt"))
-
-	assert.Equal("/params/:foo", e.Reverse("/params/:foo"))
-	assert.Equal("/params/one", e.Reverse("/params/:foo", "one"))
-	assert.Equal("/params/:foo/bar/:qux", e.Reverse("/params/:foo/bar/:qux"))
-	assert.Equal("/params/one/bar/:qux", e.Reverse("/params/:foo/bar/:qux", "one"))
-	assert.Equal("/params/one/bar/two", e.Reverse("/params/:foo/bar/:qux", "one", "two"))
-	assert.Equal("/params/one/bar/two/three", e.Reverse("/params/:foo/bar/:qux/*", "one", "two", "three"))
+	assert.Equal(t, "/static", e.Reverse("/static"))
+	assert.Equal(t, "/static", e.Reverse("/static", "missing param"))
+	assert.Equal(t, "/static/*", e.Reverse("/static/*"))
+	assert.Equal(t, "/static/foo.txt", e.Reverse("/static/*", "foo.txt"))
+
+	assert.Equal(t, "/params/:foo", e.Reverse("/params/:foo"))
+	assert.Equal(t, "/params/one", e.Reverse("/params/:foo", "one"))
+	assert.Equal(t, "/params/:foo/bar/:qux", e.Reverse("/params/:foo/bar/:qux"))
+	assert.Equal(t, "/params/one/bar/:qux", e.Reverse("/params/:foo/bar/:qux", "one"))
+	assert.Equal(t, "/params/one/bar/two", e.Reverse("/params/:foo/bar/:qux", "one", "two"))
+	assert.Equal(t, "/params/one/bar/two/three", e.Reverse("/params/:foo/bar/:qux/*", "one", "two", "three"))
 }
 
 func TestEchoReverseHandleHostProperly(t *testing.T) {
-	assert := assert.New(t)
-
 	dummyHandler := func(Context) error { return nil }
 
 	e := New()
+
+	// routes added to the default router are different form different hosts
+	e.GET("/static", dummyHandler).Name = "default-host /static"
+	e.GET("/static/*", dummyHandler).Name = "xxx"
+
+	// different host
 	h := e.Host("the_host")
-	h.GET("/static", dummyHandler).Name = "/static"
-	h.GET("/static/*", dummyHandler).Name = "/static/*"
+	h.GET("/static", dummyHandler).Name = "host2 /static"
+	h.GET("/static/v2/*", dummyHandler).Name = "xxx"
+
+	assert.Equal(t, "/static", e.Reverse("default-host /static"))
+	// when actual route does not have params and we provide some to Reverse we should get that route url back
+	assert.Equal(t, "/static", e.Reverse("default-host /static", "missing param"))
+
+	host2Router := e.Routers()["the_host"]
+	assert.Equal(t, "/static", host2Router.Reverse("host2 /static"))
+	assert.Equal(t, "/static", host2Router.Reverse("host2 /static", "missing param"))
+
+	assert.Equal(t, "/static/v2/*", host2Router.Reverse("xxx"))
+	assert.Equal(t, "/static/v2/foo.txt", host2Router.Reverse("xxx", "foo.txt"))
 
-	assert.Equal("/static", e.Reverse("/static"))
-	assert.Equal("/static", e.Reverse("/static", "missing param"))
-	assert.Equal("/static/*", e.Reverse("/static/*"))
-	assert.Equal("/static/foo.txt", e.Reverse("/static/*", "foo.txt"))
 }
 
 func TestEcho_ListenerAddr(t *testing.T) {
@@ -1485,9 +1582,9 @@ func TestEcho_ListenerAddr(t *testing.T) {
 }
 
 func TestEcho_TLSListenerAddr(t *testing.T) {
-	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	cert, err := os.ReadFile("_fixture/certs/cert.pem")
 	require.NoError(t, err)
-	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	key, err := os.ReadFile("_fixture/certs/key.pem")
 	require.NoError(t, err)
 
 	e := New()
@@ -1505,9 +1602,9 @@ func TestEcho_TLSListenerAddr(t *testing.T) {
 }
 
 func TestEcho_StartServer(t *testing.T) {
-	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	cert, err := os.ReadFile("_fixture/certs/cert.pem")
 	require.NoError(t, err)
-	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	key, err := os.ReadFile("_fixture/certs/key.pem")
 	require.NoError(t, err)
 	certs, err := tls.X509KeyPair(cert, key)
 	require.NoError(t, err)
diff --git a/group_test.go b/group_test.go
index 24f1916..01c304d 100644
--- a/group_test.go
+++ b/group_test.go
@@ -1,9 +1,9 @@
 package echo
 
 import (
-	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -32,7 +32,7 @@ func TestGroupFile(t *testing.T) {
 	e := New()
 	g := e.Group("/group")
 	g.File("/walle", "_fixture/images/walle.png")
-	expectedData, err := ioutil.ReadFile("_fixture/images/walle.png")
+	expectedData, err := os.ReadFile("_fixture/images/walle.png")
 	assert.Nil(t, err)
 	req := httptest.NewRequest(http.MethodGet, "/group/walle", nil)
 	rec := httptest.NewRecorder()
diff --git a/ip_test.go b/ip_test.go
index 755900d..38c4a1c 100644
--- a/ip_test.go
+++ b/ip_test.go
@@ -459,6 +459,7 @@ func TestExtractIPDirect(t *testing.T) {
 
 func TestExtractIPFromRealIPHeader(t *testing.T) {
 	_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR("203.0.113.199/24")
+	_, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR("2001:db8::/64")
 
 	var testCases = []struct {
 		name              string
@@ -493,6 +494,16 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.1",
 		},
+		{
+			name: "request is from external IP has valid + UNTRUSTED external X-Real-Ip header, extract IP from remote addr",
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXRealIP: []string{"[2001:db8::113:199]"}, // <-- this is untrusted
+				},
+				RemoteAddr: "[2001:db8::113:1]:8080",
+			},
+			expectIP: "2001:db8::113:1",
+		},
 		{
 			name: "request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
 			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
@@ -506,6 +517,19 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.199",
 		},
+		{
+			name: "request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
+			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
+				TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64"
+			},
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXRealIP: []string{"[2001:db8::113:199]"},
+				},
+				RemoteAddr: "[2001:db8::113:1]:8080",
+			},
+			expectIP: "2001:db8::113:199",
+		},
 		{
 			name: "request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
 			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
@@ -520,6 +544,20 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.199",
 		},
+		{
+			name: "request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
+			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
+				TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64"
+			},
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXRealIP:       []string{"[2001:db8::113:199]"},
+					HeaderXForwardedFor: []string{"[2001:db8::113:198], [2001:db8::113:197]"}, // <-- should not affect anything
+				},
+				RemoteAddr: "[2001:db8::113:1]:8080",
+			},
+			expectIP: "2001:db8::113:199",
+		},
 	}
 
 	for _, tc := range testCases {
@@ -532,6 +570,7 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 
 func TestExtractIPFromXFFHeader(t *testing.T) {
 	_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR("203.0.113.199/24")
+	_, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR("2001:db8::/64")
 
 	var testCases = []struct {
 		name              string
@@ -566,6 +605,16 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
 			},
 			expectIP: "127.0.0.3",
 		},
+		{
+			name: "request trusts all IPs in XFF header, extract IP from furthest in XFF chain",
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXForwardedFor: []string{"[fe80::3], [fe80::2], [fe80::1]"},
+				},
+				RemoteAddr: "[fe80::1]:8080",
+			},
+			expectIP: "fe80::3",
+		},
 		{
 			name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr",
 			whenRequest: http.Request{
@@ -576,6 +625,16 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.1",
 		},
+		{
+			name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr",
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXForwardedFor: []string{"[2001:db8::1]"}, // <-- this is untrusted
+				},
+				RemoteAddr: "[2001:db8::2]:8080",
+			},
+			expectIP: "2001:db8::2",
+		},
 		{
 			name: "request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header",
 			givenTrustOptions: []TrustOption{
@@ -595,6 +654,25 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
 			},
 			expectIP: "203.0.100.100", // this is first trusted IP in XFF chain
 		},
+		{
+			name: "request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header",
+			givenTrustOptions: []TrustOption{
+				TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64"
+			},
+			// from request its seems that request has been proxied through 6 servers.
+			// 1) 2001:db8:1::1:100 (this is external IP set by 2001:db8:2::100:100 which we do not trust - could be spoofed)
+			// 2) 2001:db8:2::100:100  (this is outside of our network but set by 2001:db8::113:199 which we trust to set correct IPs)
+			// 3) 2001:db8::113:199 (we trust, for example maybe our proxy from some other office)
+			// 4) fd12:3456:789a:1::1 (internal IP, some internal upstream loadbalancer ala SSL offloading with F5 products)
+			// 5) fe80::1 (is proxy on localhost. maybe we have Nginx in front of our Echo instance doing some routing)
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXForwardedFor: []string{"[2001:db8:1::1:100], [2001:db8:2::100:100], [2001:db8::113:199], [fd12:3456:789a:1::1]"},
+				},
+				RemoteAddr: "[fe80::1]:8080", // IP of proxy upstream of our APP
+			},
+			expectIP: "2001:db8:2::100:100", // this is first trusted IP in XFF chain
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/middleware/basic_auth_test.go b/middleware/basic_auth_test.go
index 4c355aa..20e7692 100644
--- a/middleware/basic_auth_test.go
+++ b/middleware/basic_auth_test.go
@@ -26,12 +26,10 @@ func TestBasicAuth(t *testing.T) {
 		return c.String(http.StatusOK, "test")
 	})
 
-	assert := assert.New(t)
-
 	// Valid credentials
 	auth := basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret"))
 	req.Header.Set(echo.HeaderAuthorization, auth)
-	assert.NoError(h(c))
+	assert.NoError(t, h(c))
 
 	h = BasicAuthWithConfig(BasicAuthConfig{
 		Skipper:   nil,
@@ -44,34 +42,34 @@ func TestBasicAuth(t *testing.T) {
 	// Valid credentials
 	auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret"))
 	req.Header.Set(echo.HeaderAuthorization, auth)
-	assert.NoError(h(c))
+	assert.NoError(t, h(c))
 
 	// Case-insensitive header scheme
 	auth = strings.ToUpper(basic) + " " + base64.StdEncoding.EncodeToString([]byte("joe:secret"))
 	req.Header.Set(echo.HeaderAuthorization, auth)
-	assert.NoError(h(c))
+	assert.NoError(t, h(c))
 
 	// Invalid credentials
 	auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:invalid-password"))
 	req.Header.Set(echo.HeaderAuthorization, auth)
 	he := h(c).(*echo.HTTPError)
-	assert.Equal(http.StatusUnauthorized, he.Code)
-	assert.Equal(basic+` realm="someRealm"`, res.Header().Get(echo.HeaderWWWAuthenticate))
+	assert.Equal(t, http.StatusUnauthorized, he.Code)
+	assert.Equal(t, basic+` realm="someRealm"`, res.Header().Get(echo.HeaderWWWAuthenticate))
 
 	// Invalid base64 string
 	auth = basic + " invalidString"
 	req.Header.Set(echo.HeaderAuthorization, auth)
 	he = h(c).(*echo.HTTPError)
-	assert.Equal(http.StatusBadRequest, he.Code)
+	assert.Equal(t, http.StatusBadRequest, he.Code)
 
 	// Missing Authorization header
 	req.Header.Del(echo.HeaderAuthorization)
 	he = h(c).(*echo.HTTPError)
-	assert.Equal(http.StatusUnauthorized, he.Code)
+	assert.Equal(t, http.StatusUnauthorized, he.Code)
 
 	// Invalid Authorization header
 	auth = base64.StdEncoding.EncodeToString([]byte("invalid"))
 	req.Header.Set(echo.HeaderAuthorization, auth)
 	he = h(c).(*echo.HTTPError)
-	assert.Equal(http.StatusUnauthorized, he.Code)
+	assert.Equal(t, http.StatusUnauthorized, he.Code)
 }
diff --git a/middleware/body_dump_test.go b/middleware/body_dump_test.go
index e6e00f7..de1de33 100644
--- a/middleware/body_dump_test.go
+++ b/middleware/body_dump_test.go
@@ -2,7 +2,7 @@ package middleware
 
 import (
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -19,7 +19,7 @@ func TestBodyDump(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	h := func(c echo.Context) error {
-		body, err := ioutil.ReadAll(c.Request().Body)
+		body, err := io.ReadAll(c.Request().Body)
 		if err != nil {
 			return err
 		}
@@ -33,13 +33,11 @@ func TestBodyDump(t *testing.T) {
 		responseBody = string(resBody)
 	})
 
-	assert := assert.New(t)
-
-	if assert.NoError(mw(h)(c)) {
-		assert.Equal(requestBody, hw)
-		assert.Equal(responseBody, hw)
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(hw, rec.Body.String())
+	if assert.NoError(t, mw(h)(c)) {
+		assert.Equal(t, requestBody, hw)
+		assert.Equal(t, responseBody, hw)
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, hw, rec.Body.String())
 	}
 
 	// Must set default skipper
diff --git a/middleware/body_limit_test.go b/middleware/body_limit_test.go
index 8981534..2bfce37 100644
--- a/middleware/body_limit_test.go
+++ b/middleware/body_limit_test.go
@@ -2,7 +2,7 @@ package middleware
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -18,33 +18,31 @@ func TestBodyLimit(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	h := func(c echo.Context) error {
-		body, err := ioutil.ReadAll(c.Request().Body)
+		body, err := io.ReadAll(c.Request().Body)
 		if err != nil {
 			return err
 		}
 		return c.String(http.StatusOK, string(body))
 	}
 
-	assert := assert.New(t)
-
 	// Based on content length (within limit)
-	if assert.NoError(BodyLimit("2M")(h)(c)) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(hw, rec.Body.Bytes())
+	if assert.NoError(t, BodyLimit("2M")(h)(c)) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, hw, rec.Body.Bytes())
 	}
 
 	// Based on content length (overlimit)
 	he := BodyLimit("2B")(h)(c).(*echo.HTTPError)
-	assert.Equal(http.StatusRequestEntityTooLarge, he.Code)
+	assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code)
 
 	// Based on content read (within limit)
 	req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw))
 	req.ContentLength = -1
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec)
-	if assert.NoError(BodyLimit("2M")(h)(c)) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal("Hello, World!", rec.Body.String())
+	if assert.NoError(t, BodyLimit("2M")(h)(c)) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, "Hello, World!", rec.Body.String())
 	}
 
 	// Based on content read (overlimit)
@@ -53,7 +51,7 @@ func TestBodyLimit(t *testing.T) {
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec)
 	he = BodyLimit("2B")(h)(c).(*echo.HTTPError)
-	assert.Equal(http.StatusRequestEntityTooLarge, he.Code)
+	assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code)
 }
 
 func TestBodyLimitReader(t *testing.T) {
@@ -69,19 +67,108 @@ func TestBodyLimitReader(t *testing.T) {
 	}
 	reader := &limitedReader{
 		BodyLimitConfig: config,
-		reader:          ioutil.NopCloser(bytes.NewReader(hw)),
+		reader:          io.NopCloser(bytes.NewReader(hw)),
 		context:         e.NewContext(req, rec),
 	}
 
 	// read all should return ErrStatusRequestEntityTooLarge
-	_, err := ioutil.ReadAll(reader)
+	_, err := io.ReadAll(reader)
 	he := err.(*echo.HTTPError)
 	assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code)
 
 	// reset reader and read two bytes must succeed
 	bt := make([]byte, 2)
-	reader.Reset(ioutil.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec))
+	reader.Reset(io.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec))
 	n, err := reader.Read(bt)
 	assert.Equal(t, 2, n)
 	assert.Equal(t, nil, err)
 }
+
+func TestBodyLimitWithConfig_Skipper(t *testing.T) {
+	e := echo.New()
+	h := func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return err
+		}
+		return c.String(http.StatusOK, string(body))
+	}
+	mw := BodyLimitWithConfig(BodyLimitConfig{
+		Skipper: func(c echo.Context) bool {
+			return true
+		},
+		Limit: "2B", // if not skipped this limit would make request to fail limit check
+	})
+
+	hw := []byte("Hello, World!")
+	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw))
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	err := mw(h)(c)
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, rec.Code)
+	assert.Equal(t, hw, rec.Body.Bytes())
+}
+
+func TestBodyLimitWithConfig(t *testing.T) {
+	var testCases = []struct {
+		name        string
+		givenLimit  string
+		whenBody    []byte
+		expectBody  []byte
+		expectError string
+	}{
+		{
+			name:        "ok, body is less than limit",
+			givenLimit:  "10B",
+			whenBody:    []byte("123456789"),
+			expectBody:  []byte("123456789"),
+			expectError: "",
+		},
+		{
+			name:        "nok, body is more than limit",
+			givenLimit:  "9B",
+			whenBody:    []byte("1234567890"),
+			expectBody:  []byte(nil),
+			expectError: "code=413, message=Request Entity Too Large",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
+			h := func(c echo.Context) error {
+				body, err := io.ReadAll(c.Request().Body)
+				if err != nil {
+					return err
+				}
+				return c.String(http.StatusOK, string(body))
+			}
+			mw := BodyLimitWithConfig(BodyLimitConfig{
+				Limit: tc.givenLimit,
+			})
+
+			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.whenBody))
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
+
+			err := mw(h)(c)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+			// not testing status as middlewares return error instead of committing it and OK cases are anyway 200
+			assert.Equal(t, tc.expectBody, rec.Body.Bytes())
+		})
+	}
+}
+
+func TestBodyLimit_panicOnInvalidLimit(t *testing.T) {
+	assert.PanicsWithError(
+		t,
+		"echo: invalid body-limit=",
+		func() { BodyLimit("") },
+	)
+}
diff --git a/middleware/compress_test.go b/middleware/compress_test.go
index b62bffe..714548e 100644
--- a/middleware/compress_test.go
+++ b/middleware/compress_test.go
@@ -4,9 +4,9 @@ import (
 	"bytes"
 	"compress/gzip"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"testing"
 
 	"github.com/labstack/echo/v4"
@@ -26,9 +26,7 @@ func TestGzip(t *testing.T) {
 	})
 	h(c)
 
-	assert := assert.New(t)
-
-	assert.Equal("test", rec.Body.String())
+	assert.Equal(t, "test", rec.Body.String())
 
 	// Gzip
 	req = httptest.NewRequest(http.MethodGet, "/", nil)
@@ -36,14 +34,14 @@ func TestGzip(t *testing.T) {
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec)
 	h(c)
-	assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
-	assert.Contains(rec.Header().Get(echo.HeaderContentType), echo.MIMETextPlain)
+	assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+	assert.Contains(t, rec.Header().Get(echo.HeaderContentType), echo.MIMETextPlain)
 	r, err := gzip.NewReader(rec.Body)
-	if assert.NoError(err) {
+	if assert.NoError(t, err) {
 		buf := new(bytes.Buffer)
 		defer r.Close()
 		buf.ReadFrom(r)
-		assert.Equal("test", buf.String())
+		assert.Equal(t, "test", buf.String())
 	}
 
 	chunkBuf := make([]byte, 5)
@@ -63,21 +61,21 @@ func TestGzip(t *testing.T) {
 		c.Response().Flush()
 
 		// Read the first part of the data
-		assert.True(rec.Flushed)
-		assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+		assert.True(t, rec.Flushed)
+		assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
 		r.Reset(rec.Body)
 
 		_, err = io.ReadFull(r, chunkBuf)
-		assert.NoError(err)
-		assert.Equal("test\n", string(chunkBuf))
+		assert.NoError(t, err)
+		assert.Equal(t, "test\n", string(chunkBuf))
 
 		// Write and flush the second part of the data
 		c.Response().Write([]byte("test\n"))
 		c.Response().Flush()
 
 		_, err = io.ReadFull(r, chunkBuf)
-		assert.NoError(err)
-		assert.Equal("test\n", string(chunkBuf))
+		assert.NoError(t, err)
+		assert.Equal(t, "test\n", string(chunkBuf))
 
 		// Write the final part of the data and return
 		c.Response().Write([]byte("test"))
@@ -87,7 +85,7 @@ func TestGzip(t *testing.T) {
 	buf := new(bytes.Buffer)
 	defer r.Close()
 	buf.ReadFrom(r)
-	assert.Equal("test", buf.String())
+	assert.Equal(t, "test", buf.String())
 }
 
 func TestGzipNoContent(t *testing.T) {
@@ -175,7 +173,7 @@ func TestGzipWithStatic(t *testing.T) {
 	r, err := gzip.NewReader(rec.Body)
 	if assert.NoError(t, err) {
 		defer r.Close()
-		want, err := ioutil.ReadFile("../_fixture/images/walle.png")
+		want, err := os.ReadFile("../_fixture/images/walle.png")
 		if assert.NoError(t, err) {
 			buf := new(bytes.Buffer)
 			buf.ReadFrom(r)
diff --git a/middleware/context_timeout_test.go b/middleware/context_timeout_test.go
new file mode 100644
index 0000000..605ca8e
--- /dev/null
+++ b/middleware/context_timeout_test.go
@@ -0,0 +1,226 @@
+package middleware
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestContextTimeoutSkipper(t *testing.T) {
+	t.Parallel()
+	m := ContextTimeoutWithConfig(ContextTimeoutConfig{
+		Skipper: func(context echo.Context) bool {
+			return true
+		},
+		Timeout: 10 * time.Millisecond,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		if err := sleepWithContext(c.Request().Context(), time.Duration(20*time.Millisecond)); err != nil {
+			return err
+		}
+
+		return errors.New("response from handler")
+	})(c)
+
+	// if not skipped we would have not returned error due context timeout logic
+	assert.EqualError(t, err, "response from handler")
+}
+
+func TestContextTimeoutWithTimeout0(t *testing.T) {
+	t.Parallel()
+	assert.Panics(t, func() {
+		ContextTimeout(time.Duration(0))
+	})
+}
+
+func TestContextTimeoutErrorOutInHandler(t *testing.T) {
+	t.Parallel()
+	m := ContextTimeoutWithConfig(ContextTimeoutConfig{
+		// Timeout has to be defined or the whole flow for timeout middleware will be skipped
+		Timeout: 10 * time.Millisecond,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	rec.Code = 1 // we want to be sure that even 200 will not be sent
+	err := m(func(c echo.Context) error {
+		// this error must not be written to the client response. Middlewares upstream of timeout middleware must be able
+		// to handle returned error and this can be done only then handler has not yet committed (written status code)
+		// the response.
+		return echo.NewHTTPError(http.StatusTeapot, "err")
+	})(c)
+
+	assert.Error(t, err)
+	assert.EqualError(t, err, "code=418, message=err")
+	assert.Equal(t, 1, rec.Code)
+	assert.Equal(t, "", rec.Body.String())
+}
+
+func TestContextTimeoutSuccessfulRequest(t *testing.T) {
+	t.Parallel()
+	m := ContextTimeoutWithConfig(ContextTimeoutConfig{
+		// Timeout has to be defined or the whole flow for timeout middleware will be skipped
+		Timeout: 10 * time.Millisecond,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		return c.JSON(http.StatusCreated, map[string]string{"data": "ok"})
+	})(c)
+
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusCreated, rec.Code)
+	assert.Equal(t, "{\"data\":\"ok\"}\n", rec.Body.String())
+}
+
+func TestContextTimeoutTestRequestClone(t *testing.T) {
+	t.Parallel()
+	req := httptest.NewRequest(http.MethodPost, "/uri?query=value", strings.NewReader(url.Values{"form": {"value"}}.Encode()))
+	req.AddCookie(&http.Cookie{Name: "cookie", Value: "value"})
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rec := httptest.NewRecorder()
+
+	m := ContextTimeoutWithConfig(ContextTimeoutConfig{
+		// Timeout has to be defined or the whole flow for timeout middleware will be skipped
+		Timeout: 1 * time.Second,
+	})
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		// Cookie test
+		cookie, err := c.Request().Cookie("cookie")
+		if assert.NoError(t, err) {
+			assert.EqualValues(t, "cookie", cookie.Name)
+			assert.EqualValues(t, "value", cookie.Value)
+		}
+
+		// Form values
+		if assert.NoError(t, c.Request().ParseForm()) {
+			assert.EqualValues(t, "value", c.Request().FormValue("form"))
+		}
+
+		// Query string
+		assert.EqualValues(t, "value", c.Request().URL.Query()["query"][0])
+		return nil
+	})(c)
+
+	assert.NoError(t, err)
+}
+
+func TestContextTimeoutWithDefaultErrorMessage(t *testing.T) {
+	t.Parallel()
+
+	timeout := 10 * time.Millisecond
+	m := ContextTimeoutWithConfig(ContextTimeoutConfig{
+		Timeout: timeout,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		if err := sleepWithContext(c.Request().Context(), time.Duration(20*time.Millisecond)); err != nil {
+			return err
+		}
+		return c.String(http.StatusOK, "Hello, World!")
+	})(c)
+
+	assert.IsType(t, &echo.HTTPError{}, err)
+	assert.Error(t, err)
+	assert.Equal(t, http.StatusServiceUnavailable, err.(*echo.HTTPError).Code)
+	assert.Equal(t, "Service Unavailable", err.(*echo.HTTPError).Message)
+}
+
+func TestContextTimeoutCanHandleContextDeadlineOnNextHandler(t *testing.T) {
+	t.Parallel()
+
+	timeoutErrorHandler := func(err error, c echo.Context) error {
+		if err != nil {
+			if errors.Is(err, context.DeadlineExceeded) {
+				return &echo.HTTPError{
+					Code:    http.StatusServiceUnavailable,
+					Message: "Timeout! change me",
+				}
+			}
+			return err
+		}
+		return nil
+	}
+
+	timeout := 10 * time.Millisecond
+	m := ContextTimeoutWithConfig(ContextTimeoutConfig{
+		Timeout:      timeout,
+		ErrorHandler: timeoutErrorHandler,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
+		// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
+		// difference over 500microseconds (0.5millisecond) response seems to be reliable
+
+		if err := sleepWithContext(c.Request().Context(), time.Duration(20*time.Millisecond)); err != nil {
+			return err
+		}
+
+		// The Request Context should have a Deadline set by http.ContextTimeoutHandler
+		if _, ok := c.Request().Context().Deadline(); !ok {
+			assert.Fail(t, "No timeout set on Request Context")
+		}
+		return c.String(http.StatusOK, "Hello, World!")
+	})(c)
+
+	assert.IsType(t, &echo.HTTPError{}, err)
+	assert.Error(t, err)
+	assert.Equal(t, http.StatusServiceUnavailable, err.(*echo.HTTPError).Code)
+	assert.Equal(t, "Timeout! change me", err.(*echo.HTTPError).Message)
+}
+
+func sleepWithContext(ctx context.Context, d time.Duration) error {
+	timer := time.NewTimer(d)
+
+	defer func() {
+		_ = timer.Stop()
+	}()
+
+	select {
+	case <-ctx.Done():
+		return context.DeadlineExceeded
+	case <-timer.C:
+		return nil
+	}
+}
diff --git a/middleware/decompress_test.go b/middleware/decompress_test.go
index 51fa6b0..2e73ba8 100644
--- a/middleware/decompress_test.go
+++ b/middleware/decompress_test.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -28,8 +28,7 @@ func TestDecompress(t *testing.T) {
 	})
 	h(c)
 
-	assert := assert.New(t)
-	assert.Equal("test", rec.Body.String())
+	assert.Equal(t, "test", rec.Body.String())
 
 	// Decompress
 	body := `{"name": "echo"}`
@@ -39,10 +38,10 @@ func TestDecompress(t *testing.T) {
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec)
 	h(c)
-	assert.Equal(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
-	assert.NoError(err)
-	assert.Equal(body, string(b))
+	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
+	b, err := io.ReadAll(req.Body)
+	assert.NoError(t, err)
+	assert.Equal(t, body, string(b))
 }
 
 func TestDecompressDefaultConfig(t *testing.T) {
@@ -57,8 +56,7 @@ func TestDecompressDefaultConfig(t *testing.T) {
 	})
 	h(c)
 
-	assert := assert.New(t)
-	assert.Equal("test", rec.Body.String())
+	assert.Equal(t, "test", rec.Body.String())
 
 	// Decompress
 	body := `{"name": "echo"}`
@@ -68,10 +66,10 @@ func TestDecompressDefaultConfig(t *testing.T) {
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec)
 	h(c)
-	assert.Equal(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
-	assert.NoError(err)
-	assert.Equal(body, string(b))
+	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
+	b, err := io.ReadAll(req.Body)
+	assert.NoError(t, err)
+	assert.Equal(t, body, string(b))
 }
 
 func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) {
@@ -84,7 +82,7 @@ func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) {
 	e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
+	b, err := io.ReadAll(req.Body)
 	assert.NoError(t, err)
 	assert.NotEqual(t, b, body)
 	assert.Equal(t, b, gz)
@@ -134,7 +132,7 @@ func TestDecompressSkipper(t *testing.T) {
 	c := e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8)
-	reqBody, err := ioutil.ReadAll(c.Request().Body)
+	reqBody, err := io.ReadAll(c.Request().Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(reqBody))
 }
@@ -163,7 +161,7 @@ func TestDecompressPoolError(t *testing.T) {
 	c := e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	reqBody, err := ioutil.ReadAll(c.Request().Body)
+	reqBody, err := io.ReadAll(c.Request().Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(reqBody))
 	assert.Equal(t, rec.Code, http.StatusInternalServerError)
diff --git a/middleware/extractor_test.go b/middleware/extractor_test.go
index 2e898f5..428c556 100644
--- a/middleware/extractor_test.go
+++ b/middleware/extractor_test.go
@@ -110,7 +110,7 @@ func TestCreateExtractors(t *testing.T) {
 				setPathParams(c, tc.givenPathParams)
 			}
 
-			extractors, err := createExtractors(tc.whenLoopups, "")
+			extractors, err := CreateExtractors(tc.whenLoopups)
 			if tc.expectCreateError != "" {
 				assert.EqualError(t, err, tc.expectCreateError)
 				return
diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go
index eee9df9..90e8cad 100644
--- a/middleware/jwt_test.go
+++ b/middleware/jwt_test.go
@@ -348,8 +348,6 @@ func TestJWTConfig(t *testing.T) {
 }
 
 func TestJWTwithKID(t *testing.T) {
-	test := assert.New(t)
-
 	e := echo.New()
 	handler := func(c echo.Context) error {
 		return c.String(http.StatusOK, "test")
@@ -417,19 +415,19 @@ func TestJWTwithKID(t *testing.T) {
 		if tc.expErrCode != 0 {
 			h := JWTWithConfig(tc.config)(handler)
 			he := h(c).(*echo.HTTPError)
-			test.Equal(tc.expErrCode, he.Code, tc.info)
+			assert.Equal(t, tc.expErrCode, he.Code, tc.info)
 			continue
 		}
 
 		h := JWTWithConfig(tc.config)(handler)
-		if test.NoError(h(c), tc.info) {
+		if assert.NoError(t, h(c), tc.info) {
 			user := c.Get("user").(*jwt.Token)
 			switch claims := user.Claims.(type) {
 			case jwt.MapClaims:
-				test.Equal(claims["name"], "John Doe", tc.info)
+				assert.Equal(t, claims["name"], "John Doe", tc.info)
 			case *jwtCustomClaims:
-				test.Equal(claims.Name, "John Doe", tc.info)
-				test.Equal(claims.Admin, true, tc.info)
+				assert.Equal(t, claims.Name, "John Doe", tc.info)
+				assert.Equal(t, claims.Admin, true, tc.info)
 			default:
 				panic("unexpected type of claims")
 			}
diff --git a/middleware/logger_test.go b/middleware/logger_test.go
index ab889bf..9f35a70 100644
--- a/middleware/logger_test.go
+++ b/middleware/logger_test.go
@@ -92,17 +92,17 @@ func TestLoggerTemplate(t *testing.T) {
 	e.Use(LoggerWithConfig(LoggerConfig{
 		Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","user_agent":"${user_agent}",` +
 			`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
-			`"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "referer":"${referer}",` +
+			`"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "route":"${route}", "referer":"${referer}",` +
 			`"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}", "protocol":"${protocol}"` +
 			`"us":"${query:username}", "cf":"${form:username}", "session":"${cookie:session}"}` + "\n",
 		Output: buf,
 	}))
 
-	e.GET("/", func(c echo.Context) error {
+	e.GET("/users/:id", func(c echo.Context) error {
 		return c.String(http.StatusOK, "Header Logged")
 	})
 
-	req := httptest.NewRequest(http.MethodGet, "/?username=apagano-param&password=secret", nil)
+	req := httptest.NewRequest(http.MethodGet, "/users/1?username=apagano-param&password=secret", nil)
 	req.RequestURI = "/"
 	req.Header.Add(echo.HeaderXRealIP, "127.0.0.1")
 	req.Header.Add("Referer", "google.com")
@@ -127,7 +127,8 @@ func TestLoggerTemplate(t *testing.T) {
 		"hexvalue":                             false,
 		"GET":                                  true,
 		"127.0.0.1":                            true,
-		"\"path\":\"/\"":                       true,
+		"\"path\":\"/users/1\"":                true,
+		"\"route\":\"/users/:id\"":             true,
 		"\"uri\":\"/\"":                        true,
 		"\"status\":200":                       true,
 		"\"bytes_in\":0":                       true,
@@ -173,6 +174,28 @@ func TestLoggerCustomTimestamp(t *testing.T) {
 	assert.Error(t, err)
 }
 
+func TestLoggerCustomTagFunc(t *testing.T) {
+	e := echo.New()
+	buf := new(bytes.Buffer)
+	e.Use(LoggerWithConfig(LoggerConfig{
+		Format: `{"method":"${method}",${custom}}` + "\n",
+		CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) {
+			return buf.WriteString(`"tag":"my-value"`)
+		},
+		Output: buf,
+	}))
+
+	e.GET("/", func(c echo.Context) error {
+		return c.String(http.StatusOK, "custom time stamp test")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, `{"method":"GET","tag":"my-value"}`+"\n", buf.String())
+}
+
 func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) {
 	e := echo.New()
 
diff --git a/middleware/proxy_test.go b/middleware/proxy_test.go
index 7939fc5..a1b7f2c 100644
--- a/middleware/proxy_test.go
+++ b/middleware/proxy_test.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -18,7 +18,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-//Assert expected with url.EscapedPath method to obtain the path.
+// Assert expected with url.EscapedPath method to obtain the path.
 func TestProxy(t *testing.T) {
 	// Setup
 	t1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -31,7 +31,6 @@ func TestProxy(t *testing.T) {
 	}))
 	defer t2.Close()
 	url2, _ := url.Parse(t2.URL)
-
 	targets := []*ProxyTarget{
 		{
 			Name: "target 1",
@@ -94,7 +93,7 @@ func TestProxy(t *testing.T) {
 	e.Use(ProxyWithConfig(ProxyConfig{
 		Balancer: rrb,
 		ModifyResponse: func(res *http.Response) error {
-			res.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("modified")))
+			res.Body = io.NopCloser(bytes.NewBuffer([]byte("modified")))
 			res.Header.Set("X-Modified", "1")
 			return nil
 		},
@@ -122,6 +121,55 @@ func TestProxy(t *testing.T) {
 	e.ServeHTTP(rec, req)
 }
 
+type testProvider struct {
+	*commonBalancer
+	target *ProxyTarget
+	err    error
+}
+
+func (p *testProvider) Next(c echo.Context) *ProxyTarget {
+	return &ProxyTarget{}
+}
+
+func (p *testProvider) NextTarget(c echo.Context) (*ProxyTarget, error) {
+	return p.target, p.err
+}
+
+func TestTargetProvider(t *testing.T) {
+	t1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprint(w, "target 1")
+	}))
+	defer t1.Close()
+	url1, _ := url.Parse(t1.URL)
+
+	e := echo.New()
+	tp := &testProvider{commonBalancer: new(commonBalancer)}
+	tp.target = &ProxyTarget{Name: "target 1", URL: url1}
+	e.Use(Proxy(tp))
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	e.ServeHTTP(rec, req)
+	body := rec.Body.String()
+	assert.Equal(t, "target 1", body)
+}
+
+func TestFailNextTarget(t *testing.T) {
+	url1, err := url.Parse("http://dummy:8080")
+	assert.Nil(t, err)
+
+	e := echo.New()
+	tp := &testProvider{commonBalancer: new(commonBalancer)}
+	tp.target = &ProxyTarget{Name: "target 1", URL: url1}
+	tp.err = echo.NewHTTPError(http.StatusInternalServerError, "method could not select target")
+
+	e.Use(Proxy(tp))
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	e.ServeHTTP(rec, req)
+	body := rec.Body.String()
+	assert.Equal(t, "{\"message\":\"method could not select target\"}\n", body)
+}
+
 func TestProxyRealIPHeader(t *testing.T) {
 	// Setup
 	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
@@ -336,10 +384,9 @@ func TestProxyError(t *testing.T) {
 	e := echo.New()
 	e.Use(Proxy(rb))
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	rec := httptest.NewRecorder()
 
 	// Remote unreachable
-	rec = httptest.NewRecorder()
+	rec := httptest.NewRecorder()
 	req.URL.Path = "/api/users"
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, "/api/users", req.URL.Path)
diff --git a/middleware/request_logger_test.go b/middleware/request_logger_test.go
index 5118b12..51d617a 100644
--- a/middleware/request_logger_test.go
+++ b/middleware/request_logger_test.go
@@ -103,12 +103,12 @@ func TestRequestLogger_beforeNextFunc(t *testing.T) {
 func TestRequestLogger_logError(t *testing.T) {
 	e := echo.New()
 
-	var expect RequestLoggerValues
+	var actual RequestLoggerValues
 	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
 		LogError:  true,
 		LogStatus: true,
 		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
-			expect = values
+			actual = values
 			return nil
 		},
 	}))
@@ -123,8 +123,52 @@ func TestRequestLogger_logError(t *testing.T) {
 	e.ServeHTTP(rec, req)
 
 	assert.Equal(t, http.StatusNotAcceptable, rec.Code)
-	assert.Equal(t, http.StatusNotAcceptable, expect.Status)
-	assert.EqualError(t, expect.Error, "code=406, message=nope")
+	assert.Equal(t, http.StatusNotAcceptable, actual.Status)
+	assert.EqualError(t, actual.Error, "code=406, message=nope")
+}
+
+func TestRequestLogger_HandleError(t *testing.T) {
+	e := echo.New()
+
+	var actual RequestLoggerValues
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		timeNow: func() time.Time {
+			return time.Unix(1631045377, 0).UTC()
+		},
+		HandleError: true,
+		LogError:    true,
+		LogStatus:   true,
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			actual = values
+			return nil
+		},
+	}))
+
+	// to see if "HandleError" works we create custom error handler that uses its own status codes
+	e.HTTPErrorHandler = func(err error, c echo.Context) {
+		if c.Response().Committed {
+			return
+		}
+		c.JSON(http.StatusTeapot, "custom error handler")
+	}
+
+	e.GET("/test", func(c echo.Context) error {
+		return echo.NewHTTPError(http.StatusForbidden, "nope")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusTeapot, rec.Code)
+
+	expect := RequestLoggerValues{
+		StartTime: time.Unix(1631045377, 0).UTC(),
+		Status:    http.StatusTeapot,
+		Error:     echo.NewHTTPError(http.StatusForbidden, "nope"),
+	}
+	assert.Equal(t, expect, actual)
 }
 
 func TestRequestLogger_LogValuesFuncError(t *testing.T) {
diff --git a/middleware/rewrite_test.go b/middleware/rewrite_test.go
index 0ac04bb..47d707c 100644
--- a/middleware/rewrite_test.go
+++ b/middleware/rewrite_test.go
@@ -1,7 +1,7 @@
 package middleware
 
 import (
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -142,7 +142,7 @@ func TestRewriteWithConfigPreMiddleware_Issue1143(t *testing.T) {
 		assert.Equal(t, http.StatusOK, rec.Code)
 
 		defer rec.Result().Body.Close()
-		bodyBytes, _ := ioutil.ReadAll(rec.Result().Body)
+		bodyBytes, _ := io.ReadAll(rec.Result().Body)
 		assert.Equal(t, "hosts", string(bodyBytes))
 	}
 }
diff --git a/middleware/timeout_test.go b/middleware/timeout_test.go
index 56eb7bc..98d96ba 100644
--- a/middleware/timeout_test.go
+++ b/middleware/timeout_test.go
@@ -5,7 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"log"
 	"net"
 	"net/http"
@@ -129,7 +129,7 @@ func TestTimeoutOnTimeoutRouteErrorHandler(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
-	stopChan := make(chan struct{}, 0)
+	stopChan := make(chan struct{})
 	err := m(func(c echo.Context) error {
 		<-stopChan
 		return errors.New("error in route after timeout")
@@ -245,7 +245,7 @@ func TestTimeoutWithErrorMessage(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
-	stopChan := make(chan struct{}, 0)
+	stopChan := make(chan struct{})
 	err := m(func(c echo.Context) error {
 		// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
 		// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
@@ -275,7 +275,7 @@ func TestTimeoutWithDefaultErrorMessage(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
-	stopChan := make(chan struct{}, 0)
+	stopChan := make(chan struct{})
 	err := m(func(c echo.Context) error {
 		<-stopChan
 		return c.String(http.StatusOK, "Hello, World!")
@@ -375,7 +375,7 @@ func TestTimeoutWithFullEchoStack(t *testing.T) {
 
 			// NOTE: timeout middleware is first as it changes Response.Writer and causes data race for logger middleware if it is not first
 			e.Use(TimeoutWithConfig(TimeoutConfig{
-				Timeout: 15 * time.Millisecond,
+				Timeout: 100 * time.Millisecond,
 			}))
 			e.Use(Logger())
 			e.Use(Recover())
@@ -403,14 +403,19 @@ func TestTimeoutWithFullEchoStack(t *testing.T) {
 			}
 			if tc.whenForceHandlerTimeout {
 				wg.Done()
+				// extremely short periods are not reliable for tests when it comes to goroutines. We can not guarantee in which
+				// order scheduler decides do execute: 1) request goroutine, 2) timeout timer goroutine.
+				// most of the time we get result we expect but Mac OS seems to be quite flaky
+				time.Sleep(50 * time.Millisecond)
+
 				// shutdown waits for server to shutdown. this way we wait logger mw to be executed
-				ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
+				ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
 				defer cancel()
 				server.Shutdown(ctx)
 			}
 
 			assert.Equal(t, tc.expectStatusCode, res.StatusCode)
-			if body, err := ioutil.ReadAll(res.Body); err == nil {
+			if body, err := io.ReadAll(res.Body); err == nil {
 				assert.Equal(t, tc.expectResponse, string(body))
 			} else {
 				assert.Fail(t, err.Error())
diff --git a/router_test.go b/router_test.go
index a954210..619cce0 100644
--- a/router_test.go
+++ b/router_test.go
@@ -674,6 +674,18 @@ func TestRouterStatic(t *testing.T) {
 	assert.Equal(t, path, c.Get("path"))
 }
 
+func TestRouterNoRoutablePath(t *testing.T) {
+	e := New()
+	r := e.router
+	c := e.NewContext(nil, nil).(*context)
+
+	r.Find(http.MethodGet, "/notfound", c)
+	c.handler(c)
+
+	// No routable path, don't set Path.
+	assert.Equal(t, "", c.Path())
+}
+
 func TestRouterParam(t *testing.T) {
 	e := New()
 	r := e.router
@@ -914,19 +926,22 @@ func TestRouterParamWithSlash(t *testing.T) {
 // Searching route for "/a/c/f" should match "/a/*/f"
 // When route `4) /a/*/f` is not added then request for "/a/c/f" should match "/:e/c/f"
 //
-//                             +----------+
-//                       +-----+ "/" root +--------------------+--------------------------+
-//                       |     +----------+                    |                          |
-//                       |                                     |                          |
-//               +-------v-------+                         +---v---------+        +-------v---+
-//               | "a/" (static) +---------------+         | ":" (param) |        | "*" (any) |
-//               +-+----------+--+               |         +-----------+-+        +-----------+
-//                 |          |                  |                     |
+//	              +----------+
+//	        +-----+ "/" root +--------------------+--------------------------+
+//	        |     +----------+                    |                          |
+//	        |                                     |                          |
+//	+-------v-------+                         +---v---------+        +-------v---+
+//	| "a/" (static) +---------------+         | ":" (param) |        | "*" (any) |
+//	+-+----------+--+               |         +-----------+-+        +-----------+
+//	  |          |                  |                     |
+//
 // +---------------v+  +-- ---v------+    +------v----+          +-----v-----------+
 // | "c/d" (static) |  | ":" (param) |    | "*" (any) |          | "/c/f" (static) |
 // +---------+------+  +--------+----+    +----------++          +-----------------+
-//           |                  |                    |
-//           |                  |                    |
+//
+//	|                  |                    |
+//	|                  |                    |
+//
 // +---------v----+      +------v--------+    +------v--------+
 // | "f" (static) |      | "/c" (static) |    | "/f" (static) |
 // +--------------+      +---------------+    +---------------+
@@ -998,22 +1013,22 @@ func TestRouteMultiLevelBacktracking(t *testing.T) {
 //
 // Request for "/a/c/f" should match "/:e/c/f"
 //
-//                            +-0,7--------+
-//                            | "/" (root) |----------------------------------+
-//                            +------------+                                  |
-//                                 |      |                                   |
-//                                 |      |                                   |
-//             +-1,6-----------+   |      |          +-8-----------+   +------v----+
-//             | "a/" (static) +<--+      +--------->+ ":" (param) |   | "*" (any) |
-//             +---------------+                     +-------------+   +-----------+
-//                |          |                             |
-//     +-2--------v-----+   +v-3,5--------+       +-9------v--------+
-//     | "c/d" (static) |   | ":" (param) |       | "/c/f" (static) |
-//     +----------------+   +-------------+       +-----------------+
-//                           |
-//                      +-4--v----------+
-//                      | "/c" (static) |
-//                      +---------------+
+//	                       +-0,7--------+
+//	                       | "/" (root) |----------------------------------+
+//	                       +------------+                                  |
+//	                            |      |                                   |
+//	                            |      |                                   |
+//	        +-1,6-----------+   |      |          +-8-----------+   +------v----+
+//	        | "a/" (static) +<--+      +--------->+ ":" (param) |   | "*" (any) |
+//	        +---------------+                     +-------------+   +-----------+
+//	           |          |                             |
+//	+-2--------v-----+   +v-3,5--------+       +-9------v--------+
+//	| "c/d" (static) |   | ":" (param) |       | "/c/f" (static) |
+//	+----------------+   +-------------+       +-----------------+
+//	                      |
+//	                 +-4--v----------+
+//	                 | "/c" (static) |
+//	                 +---------------+
 func TestRouteMultiLevelBacktracking2(t *testing.T) {
 	e := New()
 	r := e.router
@@ -2695,6 +2710,87 @@ func TestRouterHandleMethodOptions(t *testing.T) {
 	}
 }
 
+func TestRouter_Routes(t *testing.T) {
+	type rr struct {
+		method string
+		path   string
+		name   string
+	}
+	var testCases = []struct {
+		name        string
+		givenRoutes []rr
+		expect      []rr
+	}{
+		{
+			name: "ok, multiple",
+			givenRoutes: []rr{
+				{method: http.MethodGet, path: "/static", name: "/static"},
+				{method: http.MethodGet, path: "/static/*", name: "/static/*"},
+			},
+			expect: []rr{
+				{method: http.MethodGet, path: "/static", name: "/static"},
+				{method: http.MethodGet, path: "/static/*", name: "/static/*"},
+			},
+		},
+		{
+			name:        "ok, no routes",
+			givenRoutes: []rr{},
+			expect:      []rr{},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			dummyHandler := func(Context) error { return nil }
+
+			e := New()
+			route := e.router
+
+			for _, tmp := range tc.givenRoutes {
+				route.add(tmp.method, tmp.path, tmp.name, dummyHandler)
+			}
+
+			// Add does not add route. because of backwards compatibility we can not change this method signature
+			route.Add("LOCK", "/users", handlerFunc)
+
+			result := route.Routes()
+			assert.Len(t, result, len(tc.expect))
+			for _, r := range result {
+				for _, tmp := range tc.expect {
+					if tmp.name == r.Name {
+						assert.Equal(t, tmp.method, r.Method)
+						assert.Equal(t, tmp.path, r.Path)
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestRouter_Reverse(t *testing.T) {
+	e := New()
+	r := e.router
+	dummyHandler := func(Context) error { return nil }
+
+	r.add(http.MethodGet, "/static", "/static", dummyHandler)
+	r.add(http.MethodGet, "/static/*", "/static/*", dummyHandler)
+	r.add(http.MethodGet, "/params/:foo", "/params/:foo", dummyHandler)
+	r.add(http.MethodGet, "/params/:foo/bar/:qux", "/params/:foo/bar/:qux", dummyHandler)
+	r.add(http.MethodGet, "/params/:foo/bar/:qux/*", "/params/:foo/bar/:qux/*", dummyHandler)
+
+	assert.Equal(t, "/static", r.Reverse("/static"))
+	assert.Equal(t, "/static", r.Reverse("/static", "missing param"))
+	assert.Equal(t, "/static/*", r.Reverse("/static/*"))
+	assert.Equal(t, "/static/foo.txt", r.Reverse("/static/*", "foo.txt"))
+
+	assert.Equal(t, "/params/:foo", r.Reverse("/params/:foo"))
+	assert.Equal(t, "/params/one", r.Reverse("/params/:foo", "one"))
+	assert.Equal(t, "/params/:foo/bar/:qux", r.Reverse("/params/:foo/bar/:qux"))
+	assert.Equal(t, "/params/one/bar/:qux", r.Reverse("/params/:foo/bar/:qux", "one"))
+	assert.Equal(t, "/params/one/bar/two", r.Reverse("/params/:foo/bar/:qux", "one", "two"))
+	assert.Equal(t, "/params/one/bar/two/three", r.Reverse("/params/:foo/bar/:qux/*", "one", "two", "three"))
+}
+
 func TestRouterAllowHeaderForAnyOtherMethodType(t *testing.T) {
 	e := New()
 	r := e.router