Repository: labstack/echo
Pull request #1921 (2021-07-14 15:46:27)

Fixed a problem that returned wrong content-encoding when the gzip co…
I fixed a problem that returned wrong content-encoding when the gzip compressed content was empty.

`gzip.Writer` writes a gzip header if Write is called even once, and does not return the length of the header.
we need to record the Write call and make sure that the gzip header is written.



codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1921?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
> Merging [#1921](https://codecov.io/gh/labstack/echo/pull/1921?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) (c946fe1) into [master](https://codecov.io/gh/labstack/echo/commit/58366f93e60c740da21d86584ddbdad024d98a9e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) (58366f9) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1921/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)](https://codecov.io/gh/labstack/echo/pull/1921?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)

```diff
@@           Coverage Diff           @@
##           master    #1921   +/-   ##
=======================================
  Coverage   90.97%   90.97%           
=======================================
  Files          32       32           
  Lines        2792     2793    +1     
=======================================
+ Hits         2540     2541    +1     
  Misses        161      161           
  Partials       91       91           
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1921?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) | Coverage Δ | |
|---|---|---|
| [middleware/compress.go](https://codecov.io/gh/labstack/echo/pull/1921/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-bWlkZGxld2FyZS9jb21wcmVzcy5nbw==) | `84.61% <100.00%> (+0.30%)` | :arrow_up: |

------

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


Patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..af41071
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [labstack]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index ec55175..2664066 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -27,7 +27,7 @@ jobs:
         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.13, 1.14, 1.15, 1.16]
+        go: [1.14, 1.15, 1.16, 1.17]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
@@ -61,7 +61,7 @@ jobs:
           go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
 
       - name: Upload coverage to Codecov
-        if: success() && matrix.go == 1.16 && matrix.os == 'ubuntu-latest'
+        if: success() && matrix.go == 1.17 && matrix.os == 'ubuntu-latest'
         uses: codecov/codecov-action@v1
         with:
           token:
@@ -71,7 +71,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        go: [1.16]
+        go: [1.17]
     name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 892d709..f52f264 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,51 @@
 # Changelog
 
+## v4.6.1 - 2021-09-26
+
+**Enhancements**
+
+* Add start time to request logger middleware values [#1991](https://github.com/labstack/echo/pull/1991)
+
+## v4.6.0 - 2021-09-20
+
+Introduced a new [request logger](https://github.com/labstack/echo/blob/master/middleware/request_logger.go) middleware 
+to help with cases when you want to use some other logging library in your application.
+
+**Fixes**
+
+* fix timeout middleware warning: superfluous response.WriteHeader [#1905](https://github.com/labstack/echo/issues/1905)
+
+**Enhancements**
+
+* Add Cookie to KeyAuth middleware's KeyLookup [#1929](https://github.com/labstack/echo/pull/1929)
+* JWT middleware should ignore case of auth scheme in request header [#1951](https://github.com/labstack/echo/pull/1951)
+* Refactor default error handler to return first if response is already committed [#1956](https://github.com/labstack/echo/pull/1956)
+* Added request logger middleware which helps to use custom logger library for logging requests. [#1980](https://github.com/labstack/echo/pull/1980)
+* Allow escaping of colon in route path so Google Cloud API "custom methods" could be implemented [#1988](https://github.com/labstack/echo/pull/1988)
+
+## v4.5.0 - 2021-08-01
+
+**Important notes**
+
+A **BREAKING CHANGE** is introduced for JWT middleware users.
+The JWT library used for the JWT middleware had to be changed from [github.com/dgrijalva/jwt-go](https://github.com/dgrijalva/jwt-go) to
+[github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) due former library being unmaintained and affected by security
+issues.
+The [github.com/golang-jwt/jwt](https://github.com/golang-jwt/jwt) project is a drop-in replacement, but supports only the latest 2 Go versions.
+So for JWT middleware users Go 1.15+ is required. For detailed information please read [#1940](https://github.com/labstack/echo/discussions/)
+
+To change the library imports in all .go files in your project replace all occurrences of `dgrijalva/jwt-go` with `golang-jwt/jwt`.
+
+For Linux CLI you can use:
+```bash
+find -type f -name "*.go" -exec sed -i "s/dgrijalva\/jwt-go/golang-jwt\/jwt/g" {} \;
+go mod tidy
+```
+
+**Fixes**
+
+* Change JWT library to `github.com/golang-jwt/jwt` [#1946](https://github.com/labstack/echo/pull/1946)
+
 ## v4.4.0 - 2021-07-12
 
 **Fixes**
diff --git a/README.md b/README.md
index 4dec531..930cb03 100644
--- a/README.md
+++ b/README.md
@@ -66,9 +66,9 @@ go get github.com/labstack/echo/v4
 package main
 
 import (
-  "net/http"
   "github.com/labstack/echo/v4"
   "github.com/labstack/echo/v4/middleware"
+  "net/http"
 )
 
 func main() {
@@ -114,8 +114,11 @@ func hello(c echo.Context) error {
 
 ## Credits
 
-- [Vishal Rana](https://github.com/vishr) - Author
-- [Nitin Rana](https://github.com/nr17) - Consultant
+- [Vishal Rana](https://github.com/vishr) (Author)
+- [Nitin Rana](https://github.com/nr17) (Consultant)
+- [Roland Lammel](https://github.com/lammel) (Maintainer)
+- [Martti T.](https://github.com/aldas) (Maintainer)
+- [Pablo Andres Fuente](https://github.com/pafuent) (Maintainer)
 - [Contributors](https://github.com/labstack/echo/graphs/contributors)
 
 ## License
diff --git a/echo.go b/echo.go
index 406e806..ad03dd5 100644
--- a/echo.go
+++ b/echo.go
@@ -214,6 +214,7 @@ const (
 	HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
 	HeaderXRealIP             = "X-Real-IP"
 	HeaderXRequestID          = "X-Request-ID"
+	HeaderXCorrelationID      = "X-Correlation-ID"
 	HeaderXRequestedWith      = "X-Requested-With"
 	HeaderServer              = "Server"
 	HeaderOrigin              = "Origin"
@@ -241,7 +242,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.4.0"
+	Version = "4.6.1"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
@@ -357,7 +358,17 @@ func (e *Echo) Routers() map[string]*Router {
 
 // DefaultHTTPErrorHandler is the default HTTP error handler. It sends a JSON response
 // with status code.
+//
+// NOTE: In case errors happens in middleware call-chain that is returning from handler (which did not return an error).
+// When handler has already sent response (ala c.JSON()) and there is error in middleware that is returning from
+// handler. Then the error that global error handler received will be ignored because we have already "commited" the
+// response and status code header has been sent to the client.
 func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
+
+	if c.Response().Committed {
+		return
+	}
+
 	he, ok := err.(*HTTPError)
 	if ok {
 		if he.Internal != nil {
@@ -384,15 +395,13 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
 	}
 
 	// Send response
-	if !c.Response().Committed {
-		if c.Request().Method == http.MethodHead { // Issue #608
-			err = c.NoContent(he.Code)
-		} else {
-			err = c.JSON(code, message)
-		}
-		if err != nil {
-			e.Logger.Error(err)
-		}
+	if c.Request().Method == http.MethodHead { // Issue #608
+		err = c.NoContent(he.Code)
+	} else {
+		err = c.JSON(code, message)
+	}
+	if err != nil {
+		e.Logger.Error(err)
 	}
 }
 
diff --git a/go.mod b/go.mod
index 2510d10..e5fa0d5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,12 @@ module github.com/labstack/echo/v4
 go 1.15
 
 require (
-	github.com/dgrijalva/jwt-go v3.2.0+incompatible
-	github.com/labstack/gommon v0.3.0
-	github.com/mattn/go-colorable v0.1.8 // indirect
-	github.com/stretchr/testify v1.4.0
+	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-20210322153248-0c34fe9e7dc2
-	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
-	golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
-	golang.org/x/text v0.3.6 // indirect
+	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
+	golang.org/x/net v0.0.0-20210913180222-943fd674d43e
+	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
 )
diff --git a/go.sum b/go.sum
index d18f10f..8a1ec2f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,47 +1,52 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
-github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+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/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-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 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.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 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-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+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=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
+golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211104170005-ce137452f963 h1:8gJUadZl+kWvZBqG/LautX0X6qe5qTC2VI/3V3NBRAY=
+golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/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-20210910150752-751e447fb3d0/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
 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/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
+golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 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.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+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=
diff --git a/middleware/compress.go b/middleware/compress.go
index 6ae1974..ac6672e 100644
--- a/middleware/compress.go
+++ b/middleware/compress.go
@@ -27,6 +27,7 @@ type (
 	gzipResponseWriter struct {
 		io.Writer
 		http.ResponseWriter
+		wroteBody bool
 	}
 )
 
@@ -78,8 +79,9 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 				}
 				rw := res.Writer
 				w.Reset(rw)
+				grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw}
 				defer func() {
-					if res.Size == 0 {
+					if !grw.wroteBody {
 						if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {
 							res.Header().Del(echo.HeaderContentEncoding)
 						}
@@ -92,7 +94,6 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 					w.Close()
 					pool.Put(w)
 				}()
-				grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw}
 				res.Writer = grw
 			}
 			return next(c)
@@ -101,9 +102,6 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 }
 
 func (w *gzipResponseWriter) WriteHeader(code int) {
-	if code == http.StatusNoContent { // Issue #489
-		w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
-	}
 	w.Header().Del(echo.HeaderContentLength) // Issue #444
 	w.ResponseWriter.WriteHeader(code)
 }
@@ -112,6 +110,7 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
 	if w.Header().Get(echo.HeaderContentType) == "" {
 		w.Header().Set(echo.HeaderContentType, http.DetectContentType(b))
 	}
+	w.wroteBody = true
 	return w.Writer.Write(b)
 }
 
diff --git a/middleware/decompress.go b/middleware/decompress.go
index c046359..88ec709 100644
--- a/middleware/decompress.go
+++ b/middleware/decompress.go
@@ -1,10 +1,8 @@
 package middleware
 
 import (
-	"bytes"
 	"compress/gzip"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"sync"
 
@@ -43,26 +41,7 @@ type DefaultGzipDecompressPool struct {
 }
 
 func (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool {
-	return sync.Pool{
-		New: func() interface{} {
-			// create with an empty reader (but with GZIP header)
-			w, err := gzip.NewWriterLevel(ioutil.Discard, gzip.BestSpeed)
-			if err != nil {
-				return err
-			}
-
-			b := new(bytes.Buffer)
-			w.Reset(b)
-			w.Flush()
-			w.Close()
-
-			r, err := gzip.NewReader(bytes.NewReader(b.Bytes()))
-			if err != nil {
-				return err
-			}
-			return r
-		},
-	}
+	return sync.Pool{New: func() interface{} { return new(gzip.Reader) }}
 }
 
 //Decompress decompresses request body based if content encoding type is set to "gzip" with default config
@@ -82,38 +61,38 @@ func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
 		pool := config.GzipDecompressPool.gzipDecompressPool()
+
 		return func(c echo.Context) error {
 			if config.Skipper(c) {
 				return next(c)
 			}
-			switch c.Request().Header.Get(echo.HeaderContentEncoding) {
-			case GZIPEncoding:
-				b := c.Request().Body
-
-				i := pool.Get()
-				gr, ok := i.(*gzip.Reader)
-				if !ok {
-					return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error())
-				}
 
-				if err := gr.Reset(b); err != nil {
-					pool.Put(gr)
-					if err == io.EOF { //ignore if body is empty
-						return next(c)
-					}
-					return err
-				}
-				var buf bytes.Buffer
-				io.Copy(&buf, gr)
+			if c.Request().Header.Get(echo.HeaderContentEncoding) != GZIPEncoding {
+				return next(c)
+			}
 
-				gr.Close()
-				pool.Put(gr)
+			i := pool.Get()
+			gr, ok := i.(*gzip.Reader)
+			if !ok || gr == nil {
+				return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error())
+			}
+			defer pool.Put(gr)
 
-				b.Close() // http.Request.Body is closed by the Server, but because we are replacing it, it must be closed here
+			b := c.Request().Body
+			defer b.Close()
 
-				r := ioutil.NopCloser(&buf)
-				c.Request().Body = r
+			if err := gr.Reset(b); err != nil {
+				if err == io.EOF { //ignore if body is empty
+					return next(c)
+				}
+				return err
 			}
+
+			// only Close gzip reader if it was set to a proper gzip source otherwise it will panic on close.
+			defer gr.Close()
+
+			c.Request().Body = gr
+
 			return next(c)
 		}
 	}
diff --git a/middleware/jwt.go b/middleware/jwt.go
index bce4787..21e33ab 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -1,3 +1,5 @@
+// +build go1.15
+
 package middleware
 
 import (
@@ -7,7 +9,7 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	"github.com/labstack/echo/v4"
 )
 
@@ -88,7 +90,7 @@ type (
 
 		// ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token
 		// parsing fails or parsed token is invalid.
-		// Defaults to implementation using `github.com/dgrijalva/jwt-go` as JWT implementation library
+		// Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library
 		ParseTokenFunc func(auth string, c echo.Context) (interface{}, error)
 	}
 
@@ -293,7 +295,7 @@ func jwtFromHeader(header string, authScheme string) jwtExtractor {
 	return func(c echo.Context) (string, error) {
 		auth := c.Request().Header.Get(header)
 		l := len(authScheme)
-		if len(auth) > l+1 && auth[:l] == authScheme {
+		if len(auth) > l+1 && strings.EqualFold(auth[:l], authScheme) {
 			return auth[l+1:], nil
 		}
 		return "", ErrJWTMissing
diff --git a/middleware/key_auth.go b/middleware/key_auth.go
index fd169aa..54f3b47 100644
--- a/middleware/key_auth.go
+++ b/middleware/key_auth.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"errors"
+	"fmt"
 	"net/http"
 	"strings"
 
@@ -21,6 +22,7 @@ type (
 		// - "header:<name>"
 		// - "query:<name>"
 		// - "form:<name>"
+		// - "cookie:<name>"
 		KeyLookup string `yaml:"key_lookup"`
 
 		// AuthScheme to be used in the Authorization header.
@@ -91,6 +93,8 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
 		extractor = keyFromQuery(parts[1])
 	case "form":
 		extractor = keyFromForm(parts[1])
+	case "cookie":
+		extractor = keyFromCookie(parts[1])
 	}
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -164,3 +168,14 @@ func keyFromForm(param string) keyExtractor {
 		return key, nil
 	}
 }
+
+// keyFromCookie returns a `keyExtractor` that extracts key from the form.
+func keyFromCookie(cookieName string) keyExtractor {
+	return func(c echo.Context) (string, error) {
+		key, err := c.Cookie(cookieName)
+		if err != nil {
+			return "", fmt.Errorf("missing key in cookies: %w", err)
+		}
+		return key.Value, nil
+	}
+}
diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go
index 0291eb4..be2b348 100644
--- a/middleware/rate_limiter.go
+++ b/middleware/rate_limiter.go
@@ -153,9 +153,10 @@ func RateLimiterWithConfig(config RateLimiterConfig) echo.MiddlewareFunc {
 type (
 	// RateLimiterMemoryStore is the built-in store implementation for RateLimiter
 	RateLimiterMemoryStore struct {
-		visitors    map[string]*Visitor
-		mutex       sync.Mutex
-		rate        rate.Limit
+		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.
+
 		burst       int
 		expiresIn   time.Duration
 		lastCleanup time.Time
@@ -170,6 +171,8 @@ 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.
+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.
 
 Example (with 20 requests/sec):
@@ -199,7 +202,7 @@ Characteristics:
 Example:
 
 	limiterStore := middleware.NewRateLimiterMemoryStoreWithConfig(
-		middleware.RateLimiterMemoryStoreConfig{Rate: 50, Burst: 200, ExpiresIn: 5 * time.Minutes},
+		middleware.RateLimiterMemoryStoreConfig{Rate: 50, Burst: 200, ExpiresIn: 5 * time.Minute},
 	)
 */
 func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (store *RateLimiterMemoryStore) {
@@ -221,7 +224,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
+	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
 	ExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up
 }
diff --git a/middleware/request_id.go b/middleware/request_id.go
index b0baeeb..8c5ff66 100644
--- a/middleware/request_id.go
+++ b/middleware/request_id.go
@@ -17,14 +17,18 @@ type (
 
 		// RequestIDHandler defines a function which is executed for a request id.
 		RequestIDHandler func(echo.Context, string)
+
+		// TargetHeader defines what header to look for to populate the id
+		TargetHeader string
 	}
 )
 
 var (
 	// DefaultRequestIDConfig is the default RequestID middleware config.
 	DefaultRequestIDConfig = RequestIDConfig{
-		Skipper:   DefaultSkipper,
-		Generator: generator,
+		Skipper:      DefaultSkipper,
+		Generator:    generator,
+		TargetHeader: echo.HeaderXRequestID,
 	}
 )
 
@@ -42,6 +46,9 @@ func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
 	if config.Generator == nil {
 		config.Generator = generator
 	}
+	if config.TargetHeader == "" {
+		config.TargetHeader = echo.HeaderXRequestID
+	}
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
 		return func(c echo.Context) error {
@@ -51,11 +58,11 @@ func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
 
 			req := c.Request()
 			res := c.Response()
-			rid := req.Header.Get(echo.HeaderXRequestID)
+			rid := req.Header.Get(config.TargetHeader)
 			if rid == "" {
 				rid = config.Generator()
 			}
-			res.Header().Set(echo.HeaderXRequestID, rid)
+			res.Header().Set(config.TargetHeader, rid)
 			if config.RequestIDHandler != nil {
 				config.RequestIDHandler(c, rid)
 			}
diff --git a/middleware/request_logger.go b/middleware/request_logger.go
new file mode 100644
index 0000000..1b3e3ea
--- /dev/null
+++ b/middleware/request_logger.go
@@ -0,0 +1,314 @@
+package middleware
+
+import (
+	"errors"
+	"github.com/labstack/echo/v4"
+	"net/http"
+	"time"
+)
+
+// Example for `fmt.Printf`
+// 	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+//		LogStatus: true,
+//		LogURI:    true,
+//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+//			fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
+//			return nil
+//		},
+//	}))
+//
+// Example for Zerolog (https://github.com/rs/zerolog)
+// 	logger := zerolog.New(os.Stdout)
+//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+//		LogURI:    true,
+//		LogStatus: true,
+//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+//			logger.Info().
+//				Str("URI", v.URI).
+//				Int("status", v.Status).
+//				Msg("request")
+//
+//			return nil
+//		},
+//	}))
+//
+// Example for Zap (https://github.com/uber-go/zap)
+// 	logger, _ := zap.NewProduction()
+//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+//		LogURI:    true,
+//		LogStatus: true,
+//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+//			logger.Info("request",
+//				zap.String("URI", v.URI),
+//				zap.Int("status", v.Status),
+//			)
+//
+//			return nil
+//		},
+//	}))
+//
+// Example for Logrus (https://github.com/sirupsen/logrus)
+// 	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")
+//
+//			return nil
+//		},
+//	}))
+
+// RequestLoggerConfig is configuration for Request Logger middleware.
+type RequestLoggerConfig struct {
+	// Skipper defines a function to skip middleware.
+	Skipper Skipper
+
+	// BeforeNextFunc defines a function that is called before next middleware or handler is called in chain.
+	BeforeNextFunc func(c echo.Context)
+	// LogValuesFunc defines a function that is called with values extracted by logger from request/response.
+	// Mandatory.
+	LogValuesFunc func(c echo.Context, v RequestLoggerValues) error
+
+	// 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`)
+	LogProtocol bool
+	// LogRemoteIP instructs logger to extract request remote IP. See `echo.Context.RealIP()` for implementation details.
+	LogRemoteIP bool
+	// LogHost instructs logger to extract request host value (i.e. `example.com`)
+	LogHost bool
+	// LogMethod instructs logger to extract request method value (i.e. `GET` etc)
+	LogMethod bool
+	// LogURI instructs logger to extract request URI (i.e. `/list?lang=en&page=1`)
+	LogURI bool
+	// LogURIPath instructs logger to extract request URI path part (i.e. `/list`)
+	LogURIPath bool
+	// LogRoutePath instructs logger to extract route path part to which request was matched to (i.e. `/user/:id`)
+	LogRoutePath bool
+	// LogRequestID instructs logger to extract request ID from request `X-Request-ID` header or response if request did not have value.
+	LogRequestID bool
+	// LogReferer instructs logger to extract request referer values.
+	LogReferer bool
+	// LogUserAgent instructs logger to extract request user agent values.
+	LogUserAgent bool
+	// LogStatus instructs logger to extract response status code. If handler chain returns an echo.HTTPError,
+	// the status code is extracted from the echo.HTTPError returned
+	LogStatus bool
+	// LogError instructs logger to extract error returned from executed handler chain.
+	LogError bool
+	// LogContentLength instructs logger to extract content length header value. Note: this value could be different from
+	// actual request body size as it could be spoofed etc.
+	LogContentLength bool
+	// LogResponseSize instructs logger to extract response content length value. Note: when used with Gzip middleware
+	// this value may not be always correct.
+	LogResponseSize bool
+	// LogHeaders instructs logger to extract given list of headers from request. Note: request can contain more than
+	// one header with same value so slice of values is been logger for each given header.
+	//
+	// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header
+	// names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding".
+	LogHeaders []string
+	// LogQueryParams instructs logger to extract given list of query parameters from request URI. Note: request can
+	// contain more than one query parameter with same name so slice of values is been logger for each given query param name.
+	LogQueryParams []string
+	// LogFormValues instructs logger to extract given list of form values from request body+URI. Note: request can
+	// contain more than one form value with same name so slice of values is been logger for each given form value name.
+	LogFormValues []string
+
+	timeNow func() time.Time
+}
+
+// RequestLoggerValues contains extracted values from logger.
+type RequestLoggerValues struct {
+	// StartTime is time recorded before next middleware/handler is executed.
+	StartTime time.Time
+	// Latency is duration it took to execute rest of the handler chain (next(c) call).
+	Latency time.Duration
+	// Protocol is request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
+	Protocol string
+	// RemoteIP is request remote IP. See `echo.Context.RealIP()` for implementation details.
+	RemoteIP string
+	// Host is request host value (i.e. `example.com`)
+	Host string
+	// Method is request method value (i.e. `GET` etc)
+	Method string
+	// URI is request URI (i.e. `/list?lang=en&page=1`)
+	URI string
+	// URIPath is request URI path part (i.e. `/list`)
+	URIPath string
+	// RoutePath is route path part to which request was matched to (i.e. `/user/:id`)
+	RoutePath string
+	// RequestID is request ID from request `X-Request-ID` header or response if request did not have value.
+	RequestID string
+	// Referer is request referer values.
+	Referer string
+	// UserAgent is request user agent values.
+	UserAgent string
+	// Status is response status code. Then handler returns an echo.HTTPError then code from there.
+	Status int
+	// Error is error returned from executed handler chain.
+	Error error
+	// ContentLength is content length header value. Note: this value could be different from actual request body size
+	// as it could be spoofed etc.
+	ContentLength string
+	// ResponseSize is response content length value. Note: when used with Gzip middleware this value may not be always correct.
+	ResponseSize int64
+	// Headers are list of headers from request. Note: request can contain more than one header with same value so slice
+	// of values is been logger for each given header.
+	// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header
+	// names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding".
+	Headers map[string][]string
+	// QueryParams are list of query parameters from request URI. Note: request can contain more than one query parameter
+	// with same name so slice of values is been logger for each given query param name.
+	QueryParams map[string][]string
+	// FormValues are list of form values from request body+URI. Note: request can contain more than one form value with
+	// same name so slice of values is been logger for each given form value name.
+	FormValues map[string][]string
+}
+
+// RequestLoggerWithConfig returns a RequestLogger middleware with config.
+func RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc {
+	mw, err := config.ToMiddleware()
+	if err != nil {
+		panic(err)
+	}
+	return mw
+}
+
+// ToMiddleware converts RequestLoggerConfig into middleware or returns an error for invalid configuration.
+func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
+	if config.Skipper == nil {
+		config.Skipper = DefaultSkipper
+	}
+	now = time.Now
+	if config.timeNow != nil {
+		now = config.timeNow
+	}
+
+	if config.LogValuesFunc == nil {
+		return nil, errors.New("missing LogValuesFunc callback function for request logger middleware")
+	}
+
+	logHeaders := len(config.LogHeaders) > 0
+	headers := append([]string(nil), config.LogHeaders...)
+	for i, v := range headers {
+		headers[i] = http.CanonicalHeaderKey(v)
+	}
+
+	logQueryParams := len(config.LogQueryParams) > 0
+	logFormValues := len(config.LogFormValues) > 0
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) {
+				return next(c)
+			}
+
+			req := c.Request()
+			res := c.Response()
+			start := now()
+
+			if config.BeforeNextFunc != nil {
+				config.BeforeNextFunc(c)
+			}
+			err := next(c)
+
+			v := RequestLoggerValues{
+				StartTime: start,
+			}
+			if config.LogLatency {
+				v.Latency = now().Sub(start)
+			}
+			if config.LogProtocol {
+				v.Protocol = req.Proto
+			}
+			if config.LogRemoteIP {
+				v.RemoteIP = c.RealIP()
+			}
+			if config.LogHost {
+				v.Host = req.Host
+			}
+			if config.LogMethod {
+				v.Method = req.Method
+			}
+			if config.LogURI {
+				v.URI = req.RequestURI
+			}
+			if config.LogURIPath {
+				p := req.URL.Path
+				if p == "" {
+					p = "/"
+				}
+				v.URIPath = p
+			}
+			if config.LogRoutePath {
+				v.RoutePath = c.Path()
+			}
+			if config.LogRequestID {
+				id := req.Header.Get(echo.HeaderXRequestID)
+				if id == "" {
+					id = res.Header().Get(echo.HeaderXRequestID)
+				}
+				v.RequestID = id
+			}
+			if config.LogReferer {
+				v.Referer = req.Referer()
+			}
+			if config.LogUserAgent {
+				v.UserAgent = req.UserAgent()
+			}
+			if config.LogStatus {
+				v.Status = res.Status
+				if err != nil {
+					if httpErr, ok := err.(*echo.HTTPError); ok {
+						v.Status = httpErr.Code
+					}
+				}
+			}
+			if config.LogError && err != nil {
+				v.Error = err
+			}
+			if config.LogContentLength {
+				v.ContentLength = req.Header.Get(echo.HeaderContentLength)
+			}
+			if config.LogResponseSize {
+				v.ResponseSize = res.Size
+			}
+			if logHeaders {
+				v.Headers = map[string][]string{}
+				for _, header := range headers {
+					if values, ok := req.Header[header]; ok {
+						v.Headers[header] = values
+					}
+				}
+			}
+			if logQueryParams {
+				queryParams := c.QueryParams()
+				v.QueryParams = map[string][]string{}
+				for _, param := range config.LogQueryParams {
+					if values, ok := queryParams[param]; ok {
+						v.QueryParams[param] = values
+					}
+				}
+			}
+			if logFormValues {
+				v.FormValues = map[string][]string{}
+				for _, formValue := range config.LogFormValues {
+					if values, ok := req.Form[formValue]; ok {
+						v.FormValues[formValue] = values
+					}
+				}
+			}
+
+			if errOnLog := config.LogValuesFunc(c, v); errOnLog != nil {
+				return errOnLog
+			}
+
+			return err
+		}
+	}, nil
+}
diff --git a/middleware/timeout.go b/middleware/timeout.go
index 7311365..768ef8d 100644
--- a/middleware/timeout.go
+++ b/middleware/timeout.go
@@ -8,6 +8,53 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
+// ---------------------------------------------------------------------------------------------------------------
+// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
+// WARNING: Timeout middleware causes more problems than it solves.
+// WARNING: This middleware should be first middleware as it messes with request Writer and could cause data race if
+// 					it is in other position
+//
+// Depending on out requirements you could be better of setting timeout to context and
+// check its deadline from handler.
+//
+// For example: create middleware to set timeout to context
+// func RequestTimeout(timeout time.Duration) echo.MiddlewareFunc {
+//	return func(next echo.HandlerFunc) echo.HandlerFunc {
+//		return func(c echo.Context) error {
+//			timeoutCtx, cancel := context.WithTimeout(c.Request().Context(), timeout)
+//			c.SetRequest(c.Request().WithContext(timeoutCtx))
+//			defer cancel()
+//			return next(c)
+//		}
+//	}
+//}
+//
+// Create handler that checks for context deadline and runs actual task in separate coroutine
+// Note: separate coroutine may not be even if you do not want to process continue executing and
+// just want to stop long-running handler to stop and you are using "context aware" methods (ala db queries with ctx)
+// 	e.GET("/", func(c echo.Context) error {
+//
+//		doneCh := make(chan error)
+//		go func(ctx context.Context) {
+//			doneCh <- myPossiblyLongRunningBackgroundTaskWithCtx(ctx)
+//		}(c.Request().Context())
+//
+//		select { // wait for task to finish or context to timeout/cancelled
+//		case err := <-doneCh:
+//			if err != nil {
+//				return err
+//			}
+//			return c.String(http.StatusOK, "OK")
+//		case <-c.Request().Context().Done():
+//			if c.Request().Context().Err() == context.DeadlineExceeded {
+//				return c.String(http.StatusServiceUnavailable, "timeout")
+//			}
+//			return c.Request().Context().Err()
+//		}
+//
+//	})
+//
+
 type (
 	// TimeoutConfig defines the config for Timeout middleware.
 	TimeoutConfig struct {
@@ -116,13 +163,20 @@ func (t echoHandlerFuncWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Reques
 	// we restore original writer only for cases we did not timeout. On timeout we have already sent response to client
 	// and should not anymore send additional headers/data
 	// so on timeout writer stays what http.TimeoutHandler uses and prevents writing headers/body
-	t.ctx.Response().Writer = originalWriter
 	if err != nil {
-		// call global error handler to write error to the client. This is needed or `http.TimeoutHandler` will send status code by itself
-		// and after that our tries to write status code will not work anymore
+		// Error must be written into Writer created in `http.TimeoutHandler` so to get Response into `commited` state.
+		// So call global error handler to write error to the client. This is needed or `http.TimeoutHandler` will send
+		// status code by itself and after that our tries to write status code will not work anymore and/or create errors in
+		// log about `superfluous response.WriteHeader call from`
 		t.ctx.Error(err)
 		// we pass error from handler to middlewares up in handler chain to act on it if needed. But this means that
 		// global error handler is probably be called twice as `t.ctx.Error` already does that.
+
+		// NB: later call of the global error handler or middlewares will not take any effect, as echo.Response will be
+		// already marked as `committed` because we called global error handler above.
+		t.ctx.Response().Writer = originalWriter // make sure we restore before we signal original coroutine about the error
 		t.errChan <- err
+		return
 	}
+	t.ctx.Response().Writer = originalWriter
 }
diff --git a/router.go b/router.go
index 5b2474b..a8277c8 100644
--- a/router.go
+++ b/router.go
@@ -98,6 +98,9 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 
 	for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
 		if path[i] == ':' {
+			if i > 0 && path[i-1] == '\\' {
+				continue
+			}
 			j := i + 1
 
 			r.insert(method, path[:i], nil, staticKind, "", nil)


Test patch:
diff --git a/echo_test.go b/echo_test.go
index dc55349..f289158 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -1124,6 +1124,15 @@ func TestDefaultHTTPErrorHandler(t *testing.T) {
 			"error":   "stackinfo",
 		})
 	})
+	e.Any("/early-return", func(c Context) error {
+		c.String(http.StatusOK, "OK")
+		return errors.New("ERROR")
+	})
+	e.GET("/internal-error", func(c Context) error {
+		err := errors.New("internal error message body")
+		return NewHTTPError(http.StatusBadRequest).SetInternal(err)
+	})
+
 	// With Debug=true plain response contains error message
 	c, b := request(http.MethodGet, "/plain", e)
 	assert.Equal(t, http.StatusInternalServerError, c)
@@ -1136,6 +1145,14 @@ func TestDefaultHTTPErrorHandler(t *testing.T) {
 	c, b = request(http.MethodGet, "/servererror", e)
 	assert.Equal(t, http.StatusInternalServerError, c)
 	assert.Equal(t, "{\n  \"code\": 33,\n  \"error\": \"stackinfo\",\n  \"message\": \"Something bad happened\"\n}\n", b)
+	// if the body is already set HTTPErrorHandler should not add anything to response body
+	c, b = request(http.MethodGet, "/early-return", e)
+	assert.Equal(t, http.StatusOK, c)
+	assert.Equal(t, "OK", b)
+	// internal error should be reflected in the message
+	c, b = request(http.MethodGet, "/internal-error", e)
+	assert.Equal(t, http.StatusBadRequest, c)
+	assert.Equal(t, "{\n  \"error\": \"code=400, message=Bad Request, internal=internal error message body\",\n  \"message\": \"Bad Request\"\n}\n", b)
 
 	e.Debug = false
 	// With Debug=false the error response is shortened
diff --git a/middleware/compress_test.go b/middleware/compress_test.go
index d16ffca..b62bffe 100644
--- a/middleware/compress_test.go
+++ b/middleware/compress_test.go
@@ -106,6 +106,27 @@ func TestGzipNoContent(t *testing.T) {
 	}
 }
 
+func TestGzipEmpty(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	h := Gzip()(func(c echo.Context) error {
+		return c.String(http.StatusOK, "")
+	})
+	if assert.NoError(t, h(c)) {
+		assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+		assert.Equal(t, "text/plain; charset=UTF-8", rec.Header().Get(echo.HeaderContentType))
+		r, err := gzip.NewReader(rec.Body)
+		if assert.NoError(t, err) {
+			var buf bytes.Buffer
+			buf.ReadFrom(r)
+			assert.Equal(t, "", buf.String())
+		}
+	}
+}
+
 func TestGzipErrorReturned(t *testing.T) {
 	e := echo.New()
 	e.Use(Gzip())
diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go
index 9af4c83..5f36ce0 100644
--- a/middleware/jwt_test.go
+++ b/middleware/jwt_test.go
@@ -1,3 +1,5 @@
+// +build go1.15
+
 package middleware
 
 import (
@@ -9,7 +11,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/golang-jwt/jwt"
 	"github.com/labstack/echo/v4"
 	"github.com/stretchr/testify/assert"
 )
@@ -259,6 +261,11 @@ func TestJWT(t *testing.T) {
 			expErrCode: http.StatusUnauthorized,
 			info:       "Token verification does not pass using a user-defined KeyFunc",
 		},
+		{
+			hdrAuth: strings.ToLower(DefaultJWTConfig.AuthScheme) + " " + token,
+			config:  JWTConfig{SigningKey: validKey},
+			info:    "Valid JWT with lower case AuthScheme",
+		},
 	} {
 		if tc.reqURL == "" {
 			tc.reqURL = "/"
@@ -562,7 +569,7 @@ func TestJWTConfig_custom_ParseTokenFunc_Keyfunc(t *testing.T) {
 		return c.String(http.StatusTeapot, "test")
 	})
 
-	// example of minimal custom ParseTokenFunc implementation. Allows you to use different versions of `github.com/dgrijalva/jwt-go`
+	// example of minimal custom ParseTokenFunc implementation. Allows you to use different versions of `github.com/golang-jwt/jwt`
 	// with current JWT middleware
 	signingKey := []byte("secret")
 
diff --git a/middleware/key_auth_test.go b/middleware/key_auth_test.go
index 476b402..0cc513a 100644
--- a/middleware/key_auth_test.go
+++ b/middleware/key_auth_test.go
@@ -157,6 +157,30 @@ func TestKeyAuthWithConfig(t *testing.T) {
 			expectHandlerCalled: false,
 			expectError:         "code=400, message=missing key in the form",
 		},
+		{
+			name: "ok, custom key lookup, cookie",
+			givenRequest: func(req *http.Request) {
+				req.AddCookie(&http.Cookie{
+					Name:  "key",
+					Value: "valid-key",
+				})
+				q := req.URL.Query()
+				q.Add("key", "valid-key")
+				req.URL.RawQuery = q.Encode()
+			},
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "cookie:key"
+			},
+			expectHandlerCalled: true,
+		},
+		{
+			name: "nok, custom key lookup, missing cookie param",
+			whenConfig: func(conf *KeyAuthConfig) {
+				conf.KeyLookup = "cookie:key"
+			},
+			expectHandlerCalled: false,
+			expectError:         "code=400, message=missing key in cookies: http: named cookie not present",
+		},
 		{
 			name: "nok, custom errorHandler, error from extractor",
 			whenConfig: func(conf *KeyAuthConfig) {
diff --git a/middleware/logger_test.go b/middleware/logger_test.go
index 4d4515b..394f627 100644
--- a/middleware/logger_test.go
+++ b/middleware/logger_test.go
@@ -171,3 +171,76 @@ func TestLoggerCustomTimestamp(t *testing.T) {
 	_, err := time.Parse(customTimeFormat, loggedTime)
 	assert.Error(t, err)
 }
+
+func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) {
+	e := echo.New()
+
+	buf := new(bytes.Buffer)
+	mw := 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}",` +
+			`"bytes_out":${bytes_out}, "protocol":"${protocol}"}` + "\n",
+		Output: buf,
+	})(func(c echo.Context) error {
+		c.Request().Header.Set(echo.HeaderXRequestID, "123")
+		c.FormValue("to force parse form")
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	f := make(url.Values)
+	f.Set("csrf", "token")
+	f.Add("multiple", "1")
+	f.Add("multiple", "2")
+	req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", strings.NewReader(f.Encode()))
+	req.Header.Set("Referer", "https://echo.labstack.com/")
+	req.Header.Set("User-Agent", "curl/7.68.0")
+	req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		rec := httptest.NewRecorder()
+		c := e.NewContext(req, rec)
+		mw(c)
+		buf.Reset()
+	}
+}
+
+func BenchmarkLoggerWithConfig_withMapFields(b *testing.B) {
+	e := echo.New()
+
+	buf := new(bytes.Buffer)
+	mw := 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}",` +
+			`"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}", "protocol":"${protocol}"` +
+			`"us":"${query:username}", "cf":"${form:csrf}", "Referer2":"${header:Referer}"}` + "\n",
+		Output: buf,
+	})(func(c echo.Context) error {
+		c.Request().Header.Set(echo.HeaderXRequestID, "123")
+		c.FormValue("to force parse form")
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	f := make(url.Values)
+	f.Set("csrf", "token")
+	f.Add("multiple", "1")
+	f.Add("multiple", "2")
+	req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", strings.NewReader(f.Encode()))
+	req.Header.Set("Referer", "https://echo.labstack.com/")
+	req.Header.Set("User-Agent", "curl/7.68.0")
+	req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		rec := httptest.NewRecorder()
+		c := e.NewContext(req, rec)
+		mw(c)
+		buf.Reset()
+	}
+}
diff --git a/middleware/request_id_test.go b/middleware/request_id_test.go
index 944b3b4..21b7778 100644
--- a/middleware/request_id_test.go
+++ b/middleware/request_id_test.go
@@ -55,3 +55,34 @@ func TestRequestID_IDNotAltered(t *testing.T) {
 	_ = h(c)
 	assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "<sample-request-id>")
 }
+
+func TestRequestIDConfigDifferentHeader(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+	handler := func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	}
+
+	rid := RequestIDWithConfig(RequestIDConfig{TargetHeader: echo.HeaderXCorrelationID})
+	h := rid(handler)
+	h(c)
+	assert.Len(t, rec.Header().Get(echo.HeaderXCorrelationID), 32)
+
+	// Custom generator and handler
+	customID := "customGenerator"
+	calledHandler := false
+	rid = RequestIDWithConfig(RequestIDConfig{
+		Generator:    func() string { return customID },
+		TargetHeader: echo.HeaderXCorrelationID,
+		RequestIDHandler: func(_ echo.Context, id string) {
+			calledHandler = true
+			assert.Equal(t, customID, id)
+		},
+	})
+	h = rid(handler)
+	h(c)
+	assert.Equal(t, rec.Header().Get(echo.HeaderXCorrelationID), "customGenerator")
+	assert.True(t, calledHandler)
+}
diff --git a/middleware/request_logger_test.go b/middleware/request_logger_test.go
new file mode 100644
index 0000000..5118b12
--- /dev/null
+++ b/middleware/request_logger_test.go
@@ -0,0 +1,418 @@
+package middleware
+
+import (
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+)
+
+func TestRequestLoggerWithConfig(t *testing.T) {
+	e := echo.New()
+
+	var expect RequestLoggerValues
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		LogRoutePath: true,
+		LogURI:       true,
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			expect = values
+			return nil
+		},
+	}))
+
+	e.GET("/test", func(c echo.Context) error {
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusTeapot, rec.Code)
+	assert.Equal(t, "/test", expect.RoutePath)
+}
+
+func TestRequestLoggerWithConfig_missingOnLogValuesPanics(t *testing.T) {
+	assert.Panics(t, func() {
+		RequestLoggerWithConfig(RequestLoggerConfig{
+			LogValuesFunc: nil,
+		})
+	})
+}
+
+func TestRequestLogger_skipper(t *testing.T) {
+	e := echo.New()
+
+	loggerCalled := false
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		Skipper: func(c echo.Context) bool {
+			return true
+		},
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			loggerCalled = true
+			return nil
+		},
+	}))
+
+	e.GET("/test", func(c echo.Context) error {
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusTeapot, rec.Code)
+	assert.False(t, loggerCalled)
+}
+
+func TestRequestLogger_beforeNextFunc(t *testing.T) {
+	e := echo.New()
+
+	var myLoggerInstance int
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		BeforeNextFunc: func(c echo.Context) {
+			c.Set("myLoggerInstance", 42)
+		},
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			myLoggerInstance = c.Get("myLoggerInstance").(int)
+			return nil
+		},
+	}))
+
+	e.GET("/test", func(c echo.Context) error {
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusTeapot, rec.Code)
+	assert.Equal(t, 42, myLoggerInstance)
+}
+
+func TestRequestLogger_logError(t *testing.T) {
+	e := echo.New()
+
+	var expect RequestLoggerValues
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		LogError:  true,
+		LogStatus: true,
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			expect = values
+			return nil
+		},
+	}))
+
+	e.GET("/test", func(c echo.Context) error {
+		return echo.NewHTTPError(http.StatusNotAcceptable, "nope")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	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")
+}
+
+func TestRequestLogger_LogValuesFuncError(t *testing.T) {
+	e := echo.New()
+
+	var expect RequestLoggerValues
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		LogError:  true,
+		LogStatus: true,
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			expect = values
+			return echo.NewHTTPError(http.StatusNotAcceptable, "LogValuesFuncError")
+		},
+	}))
+
+	e.GET("/test", func(c echo.Context) error {
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	// NOTE: when global error handler received error returned from middleware the status has already
+	// been written to the client and response has been "commited" therefore global error handler does not do anything
+	// and error that bubbled up in middleware chain will not be reflected in response code.
+	assert.Equal(t, http.StatusTeapot, rec.Code)
+	assert.Equal(t, http.StatusTeapot, expect.Status)
+}
+
+func TestRequestLogger_ID(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		whenFromRequest bool
+		expect          string
+	}{
+		{
+			name:            "ok, ID is provided from request headers",
+			whenFromRequest: true,
+			expect:          "123",
+		},
+		{
+			name:            "ok, ID is from response headers",
+			whenFromRequest: false,
+			expect:          "321",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
+
+			var expect RequestLoggerValues
+			e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+				LogRequestID: true,
+				LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+					expect = values
+					return nil
+				},
+			}))
+
+			e.GET("/test", func(c echo.Context) error {
+				c.Response().Header().Set(echo.HeaderXRequestID, "321")
+				return c.String(http.StatusTeapot, "OK")
+			})
+
+			req := httptest.NewRequest(http.MethodGet, "/test", nil)
+			if tc.whenFromRequest {
+				req.Header.Set(echo.HeaderXRequestID, "123")
+			}
+			rec := httptest.NewRecorder()
+
+			e.ServeHTTP(rec, req)
+
+			assert.Equal(t, http.StatusTeapot, rec.Code)
+			assert.Equal(t, tc.expect, expect.RequestID)
+		})
+	}
+}
+
+func TestRequestLogger_headerIsCaseInsensitive(t *testing.T) {
+	e := echo.New()
+
+	var expect RequestLoggerValues
+	mw := RequestLoggerWithConfig(RequestLoggerConfig{
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			expect = values
+			return nil
+		},
+		LogHeaders: []string{"referer", "User-Agent"},
+	})(func(c echo.Context) error {
+		c.Request().Header.Set(echo.HeaderXRequestID, "123")
+		c.FormValue("to force parse form")
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test?lang=en&checked=1&checked=2", nil)
+	req.Header.Set("referer", "https://echo.labstack.com/")
+
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	err := mw(c)
+
+	assert.NoError(t, err)
+	assert.Len(t, expect.Headers, 1)
+	assert.Equal(t, []string{"https://echo.labstack.com/"}, expect.Headers["Referer"])
+}
+
+func TestRequestLogger_allFields(t *testing.T) {
+	e := echo.New()
+
+	isFirstNowCall := true
+	var expect RequestLoggerValues
+	mw := RequestLoggerWithConfig(RequestLoggerConfig{
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			expect = values
+			return nil
+		},
+		LogLatency:       true,
+		LogProtocol:      true,
+		LogRemoteIP:      true,
+		LogHost:          true,
+		LogMethod:        true,
+		LogURI:           true,
+		LogURIPath:       true,
+		LogRoutePath:     true,
+		LogRequestID:     true,
+		LogReferer:       true,
+		LogUserAgent:     true,
+		LogStatus:        true,
+		LogError:         true,
+		LogContentLength: true,
+		LogResponseSize:  true,
+		LogHeaders:       []string{"accept-encoding", "User-Agent"},
+		LogQueryParams:   []string{"lang", "checked"},
+		LogFormValues:    []string{"csrf", "multiple"},
+		timeNow: func() time.Time {
+			if isFirstNowCall {
+				isFirstNowCall = false
+				return time.Unix(1631045377, 0)
+			}
+			return time.Unix(1631045377+10, 0)
+		},
+	})(func(c echo.Context) error {
+		c.Request().Header.Set(echo.HeaderXRequestID, "123")
+		c.FormValue("to force parse form")
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	f := make(url.Values)
+	f.Set("csrf", "token")
+	f.Set("multiple", "1")
+	f.Add("multiple", "2")
+	reader := strings.NewReader(f.Encode())
+	req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", reader)
+	req.Header.Set("Referer", "https://echo.labstack.com/")
+	req.Header.Set("User-Agent", "curl/7.68.0")
+	req.Header.Set(echo.HeaderContentLength, strconv.Itoa(int(reader.Size())))
+	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
+	req.Header.Set(echo.HeaderXRealIP, "8.8.8.8")
+
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	c.SetPath("/test*")
+
+	err := mw(c)
+
+	assert.NoError(t, err)
+	assert.Equal(t, time.Unix(1631045377, 0), expect.StartTime)
+	assert.Equal(t, 10*time.Second, expect.Latency)
+	assert.Equal(t, "HTTP/1.1", expect.Protocol)
+	assert.Equal(t, "8.8.8.8", expect.RemoteIP)
+	assert.Equal(t, "example.com", expect.Host)
+	assert.Equal(t, http.MethodPost, expect.Method)
+	assert.Equal(t, "/test?lang=en&checked=1&checked=2", expect.URI)
+	assert.Equal(t, "/test", expect.URIPath)
+	assert.Equal(t, "/test*", expect.RoutePath)
+	assert.Equal(t, "123", expect.RequestID)
+	assert.Equal(t, "https://echo.labstack.com/", expect.Referer)
+	assert.Equal(t, "curl/7.68.0", expect.UserAgent)
+	assert.Equal(t, 418, expect.Status)
+	assert.Equal(t, nil, expect.Error)
+	assert.Equal(t, "32", expect.ContentLength)
+	assert.Equal(t, int64(2), expect.ResponseSize)
+
+	assert.Len(t, expect.Headers, 1)
+	assert.Equal(t, []string{"curl/7.68.0"}, expect.Headers["User-Agent"])
+
+	assert.Len(t, expect.QueryParams, 2)
+	assert.Equal(t, []string{"en"}, expect.QueryParams["lang"])
+	assert.Equal(t, []string{"1", "2"}, expect.QueryParams["checked"])
+
+	assert.Len(t, expect.FormValues, 2)
+	assert.Equal(t, []string{"token"}, expect.FormValues["csrf"])
+	assert.Equal(t, []string{"1", "2"}, expect.FormValues["multiple"])
+}
+
+func BenchmarkRequestLogger_withoutMapFields(b *testing.B) {
+	e := echo.New()
+
+	mw := RequestLoggerWithConfig(RequestLoggerConfig{
+		Skipper: nil,
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			return nil
+		},
+		LogLatency:       true,
+		LogProtocol:      true,
+		LogRemoteIP:      true,
+		LogHost:          true,
+		LogMethod:        true,
+		LogURI:           true,
+		LogURIPath:       true,
+		LogRoutePath:     true,
+		LogRequestID:     true,
+		LogReferer:       true,
+		LogUserAgent:     true,
+		LogStatus:        true,
+		LogError:         true,
+		LogContentLength: true,
+		LogResponseSize:  true,
+	})(func(c echo.Context) error {
+		c.Request().Header.Set(echo.HeaderXRequestID, "123")
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test?lang=en", nil)
+	req.Header.Set("Referer", "https://echo.labstack.com/")
+	req.Header.Set("User-Agent", "curl/7.68.0")
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		rec := httptest.NewRecorder()
+		c := e.NewContext(req, rec)
+		mw(c)
+	}
+}
+
+func BenchmarkRequestLogger_withMapFields(b *testing.B) {
+	e := echo.New()
+
+	mw := RequestLoggerWithConfig(RequestLoggerConfig{
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			return nil
+		},
+		LogLatency:       true,
+		LogProtocol:      true,
+		LogRemoteIP:      true,
+		LogHost:          true,
+		LogMethod:        true,
+		LogURI:           true,
+		LogURIPath:       true,
+		LogRoutePath:     true,
+		LogRequestID:     true,
+		LogReferer:       true,
+		LogUserAgent:     true,
+		LogStatus:        true,
+		LogError:         true,
+		LogContentLength: true,
+		LogResponseSize:  true,
+		LogHeaders:       []string{"accept-encoding", "User-Agent"},
+		LogQueryParams:   []string{"lang", "checked"},
+		LogFormValues:    []string{"csrf", "multiple"},
+	})(func(c echo.Context) error {
+		c.Request().Header.Set(echo.HeaderXRequestID, "123")
+		c.FormValue("to force parse form")
+		return c.String(http.StatusTeapot, "OK")
+	})
+
+	f := make(url.Values)
+	f.Set("csrf", "token")
+	f.Add("multiple", "1")
+	f.Add("multiple", "2")
+	req := httptest.NewRequest(http.MethodPost, "/test?lang=en&checked=1&checked=2", strings.NewReader(f.Encode()))
+	req.Header.Set("Referer", "https://echo.labstack.com/")
+	req.Header.Set("User-Agent", "curl/7.68.0")
+	req.Header.Add(echo.HeaderContentType, echo.MIMEApplicationForm)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		rec := httptest.NewRecorder()
+		c := e.NewContext(req, rec)
+		mw(c)
+	}
+}
diff --git a/middleware/timeout_test.go b/middleware/timeout_test.go
index 80891e8..aa6402b 100644
--- a/middleware/timeout_test.go
+++ b/middleware/timeout_test.go
@@ -1,7 +1,12 @@
 package middleware
 
 import (
+	"bytes"
 	"errors"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -313,3 +318,126 @@ func TestTimeoutCanHandleContextDeadlineOnNextHandler(t *testing.T) {
 	assert.Equal(t, "Timeout! change me", rec.Body.String())
 	assert.False(t, <-handlerFinishedExecution)
 }
+
+func TestTimeoutWithFullEchoStack(t *testing.T) {
+	// test timeout with full http server stack running, do see what http.Server.ErrorLog contains
+	var testCases = []struct {
+		name                 string
+		whenPath             string
+		expectStatusCode     int
+		expectResponse       string
+		expectLogContains    []string
+		expectLogNotContains []string
+	}{
+		{
+			name:                 "404 - write response in global error handler",
+			whenPath:             "/404",
+			expectResponse:       "{\"message\":\"Not Found\"}\n",
+			expectStatusCode:     http.StatusNotFound,
+			expectLogNotContains: []string{"echo:http: superfluous response.WriteHeader call from"},
+			expectLogContains:    []string{`"status":404,"error":"code=404, message=Not Found"`},
+		},
+		{
+			name:                 "418 - write response in handler",
+			whenPath:             "/",
+			expectResponse:       "{\"message\":\"OK\"}\n",
+			expectStatusCode:     http.StatusTeapot,
+			expectLogNotContains: []string{"echo:http: superfluous response.WriteHeader call from"},
+			expectLogContains:    []string{`"status":418,"error":"",`},
+		},
+		{
+			name:             "503 - handler timeouts, write response in timeout middleware",
+			whenPath:         "/?delay=50ms",
+			expectResponse:   "<html><head><title>Timeout</title></head><body><h1>Timeout</h1></body></html>",
+			expectStatusCode: http.StatusServiceUnavailable,
+			expectLogNotContains: []string{
+				"echo:http: superfluous response.WriteHeader call from",
+				"{", // means that logger was not called.
+			},
+		},
+	}
+
+	e := echo.New()
+
+	buf := new(bytes.Buffer)
+	e.Logger.SetOutput(buf)
+
+	// NOTE: timeout middleware is first as it changes Response.Writer and causes data race for logger middleware if it is not first
+	// FIXME: I have no idea how to fix this without adding mutexes.
+	e.Use(TimeoutWithConfig(TimeoutConfig{
+		Timeout: 15 * time.Millisecond,
+	}))
+	e.Use(Logger())
+	e.Use(Recover())
+
+	e.GET("/", func(c echo.Context) error {
+		var delay time.Duration
+		if err := echo.QueryParamsBinder(c).Duration("delay", &delay).BindError(); err != nil {
+			return err
+		}
+		if delay > 0 {
+			time.Sleep(delay)
+		}
+		return c.JSON(http.StatusTeapot, map[string]string{"message": "OK"})
+	})
+
+	server, addr, err := startServer(e)
+	if err != nil {
+		assert.NoError(t, err)
+		return
+	}
+	defer server.Close()
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			buf.Reset() // this is design this can not be run in parallel
+
+			res, err := http.Get(fmt.Sprintf("http://%v%v", addr, tc.whenPath))
+			if err != nil {
+				assert.NoError(t, err)
+				return
+			}
+
+			assert.Equal(t, tc.expectStatusCode, res.StatusCode)
+			if body, err := ioutil.ReadAll(res.Body); err == nil {
+				assert.Equal(t, tc.expectResponse, string(body))
+			} else {
+				assert.Fail(t, err.Error())
+			}
+
+			logged := buf.String()
+			for _, subStr := range tc.expectLogContains {
+				assert.True(t, strings.Contains(logged, subStr))
+			}
+			for _, subStr := range tc.expectLogNotContains {
+				assert.False(t, strings.Contains(logged, subStr))
+			}
+		})
+	}
+}
+
+func startServer(e *echo.Echo) (*http.Server, string, error) {
+	l, err := net.Listen("tcp", ":0")
+	if err != nil {
+		return nil, "", err
+	}
+
+	s := http.Server{
+		Handler:  e,
+		ErrorLog: log.New(e.Logger.Output(), "echo:", 0),
+	}
+
+	errCh := make(chan error)
+	go func() {
+		if err := s.Serve(l); err != http.ErrServerClosed {
+			errCh <- err
+		}
+	}()
+
+	select {
+	case <-time.After(10 * time.Millisecond):
+		return &s, l.Addr().String(), nil
+	case err := <-errCh:
+		return nil, "", err
+	}
+}
diff --git a/router_test.go b/router_test.go
index 71cedf8..1cb36b4 100644
--- a/router_test.go
+++ b/router_test.go
@@ -1118,6 +1118,58 @@ func TestRouterParamStaticConflict(t *testing.T) {
 	}
 }
 
+func TestRouterParam_escapeColon(t *testing.T) {
+	// to allow Google cloud API like route paths with colon in them
+	// i.e. https://service.name/v1/some/resource/name:customVerb <- that `:customVerb` is not path param. It is just a string
+	e := New()
+
+	e.POST("/files/a/long/file\\:undelete", handlerFunc)
+	e.POST("/v1/some/resource/name:customVerb", handlerFunc)
+
+	var testCases = []struct {
+		whenURL     string
+		expectRoute interface{}
+		expectParam map[string]string
+		expectError string
+	}{
+		{
+			whenURL:     "/files/a/long/file\\:undelete",
+			expectRoute: "/files/a/long/file\\:undelete",
+			expectParam: map[string]string{},
+		},
+		{
+			whenURL:     "/files/a/long/file\\:notMatching",
+			expectRoute: nil,
+			expectError: "code=404, message=Not Found",
+			expectParam: nil,
+		},
+		{
+			whenURL:     "/v1/some/resource/name:PATCH",
+			expectRoute: "/v1/some/resource/name:customVerb",
+			expectParam: map[string]string{"customVerb": ":PATCH"},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.whenURL, func(t *testing.T) {
+			c := e.NewContext(nil, nil).(*context)
+
+			e.router.Find(http.MethodPost, tc.whenURL, c)
+			err := c.handler(c)
+
+			assert.Equal(t, tc.expectRoute, c.Get("path"))
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+			for param, expectedValue := range tc.expectParam {
+				assert.Equal(t, expectedValue, c.Param(param))
+			}
+			checkUnusedParamValues(t, c, tc.expectParam)
+		})
+	}
+}
+
 func TestRouterMatchAny(t *testing.T) {
 	e := New()
 	r := e.router