Repository: labstack/echo (go)

Issue:
Binding of query parameters should also work for HEAD methods
Based on the fact that HEAD method is a GET-like with no content, I think that binding of query parameters should be allowed for HEAD methods as well here:

https://github.com/labstack/echo/blob/8b4cce5021eebda91db43f80f4a8f2ae2f949282/bind.go#L118

Thoughts?

Pull request:
Adding support for HEAD method query params binding.
Adding support for HEAD method query params binding.

Fixes #2026 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f52f264..372ed13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
 # Changelog
 
+## v4.6.3 - 2022-01-10
+
+**Fixes**
+
+* Fixed Echo version number in greeting message which was not incremented to `4.6.2` [#2066](https://github.com/labstack/echo/issues/2066)
+
+
+## v4.6.2 - 2022-01-08
+
+**Fixes**
+
+* Fixed route containing escaped colon should be matchable but is not matched to request path [#2047](https://github.com/labstack/echo/pull/2047)
+* Fixed a problem that returned wrong content-encoding when the gzip compressed content was empty. [#1921](https://github.com/labstack/echo/pull/1921)
+* Update (test) dependencies [#2021](https://github.com/labstack/echo/pull/2021)
+
+
+**Enhancements**
+
+* Add support for configurable target header for the request_id middleware [#2040](https://github.com/labstack/echo/pull/2040)
+* Change decompress middleware to use stream decompression instead of buffering [#2018](https://github.com/labstack/echo/pull/2018)
+* Documentation updates
+
+
 ## v4.6.1 - 2021-09-26
 
 **Enhancements**
diff --git a/README.md b/README.md
index 930cb03..8b2321f 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,6 @@
 [![Go Report Card](https://goreportcard.com/badge/github.com/labstack/echo?style=flat-square)](https://goreportcard.com/report/github.com/labstack/echo)
 [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo)
 [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo)
-[![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
 [![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://github.com/labstack/echo/discussions)
 [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack)
 [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE)
@@ -92,10 +91,23 @@ 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.
+
+Please send a PR to add your own library here.
+
 ## Help
 
 - [Forum](https://github.com/labstack/echo/discussions)
-- [Chat](https://gitter.im/labstack/echo)
 
 ## Contribute
 
diff --git a/bind.go b/bind.go
index fdf0524..c841ca0 100644
--- a/bind.go
+++ b/bind.go
@@ -111,11 +111,11 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
 	if err := b.BindPathParams(c, i); err != nil {
 		return err
 	}
-	// Issue #1670 - Query params are binded only for GET/DELETE and NOT for usual request with body (POST/PUT/PATCH)
-	// Reasoning here is that parameters in query and bind destination struct could have UNEXPECTED matches and results due that.
-	// i.e. is `&id=1&lang=en` from URL same as `{"id":100,"lang":"de"}` request body and which one should have priority when binding.
-	// This HTTP method check restores pre v4.1.11 behavior and avoids different problems when query is mixed with body
-	if c.Request().Method == http.MethodGet || c.Request().Method == http.MethodDelete {
+	// Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body.
+	// For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues.
+	// The HTTP method check restores pre-v4.1.11 behavior to avoid these problems (see issue #1670)
+  method := c.Request().Method
+	if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {
 		if err = b.BindQueryParams(c, i); err != nil {
 			return err
 		}
diff --git a/context.go b/context.go
index 91ab6e4..f2421d7 100644
--- a/context.go
+++ b/context.go
@@ -210,6 +210,13 @@ type (
 	}
 )
 
+const (
+	// ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain.
+	// Allow header is mandatory for status 405 (method not found) and useful for OPTIONS method requests.
+	// It is added to context only when Router does not find matching method handler for request.
+	ContextKeyHeaderAllow = "echo_header_allow"
+)
+
 const (
 	defaultMemory = 32 << 20 // 32 MB
 	indexPage     = "index.html"
diff --git a/echo.go b/echo.go
index df5d358..fc7e116 100644
--- a/echo.go
+++ b/echo.go
@@ -190,8 +190,12 @@ const (
 
 // Headers
 const (
-	HeaderAccept              = "Accept"
-	HeaderAcceptEncoding      = "Accept-Encoding"
+	HeaderAccept         = "Accept"
+	HeaderAcceptEncoding = "Accept-Encoding"
+	// HeaderAllow is the name of the "Allow" header field used to list the set of methods
+	// advertised as supported by the target resource. Returning an Allow header is mandatory
+	// for status 405 (method not found) and useful for the OPTIONS method in responses.
+	// See RFC 7231: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
 	HeaderAllow               = "Allow"
 	HeaderAuthorization       = "Authorization"
 	HeaderContentDisposition  = "Content-Disposition"
@@ -203,6 +207,7 @@ const (
 	HeaderIfModifiedSince     = "If-Modified-Since"
 	HeaderLastModified        = "Last-Modified"
 	HeaderLocation            = "Location"
+	HeaderRetryAfter          = "Retry-After"
 	HeaderUpgrade             = "Upgrade"
 	HeaderVary                = "Vary"
 	HeaderWWWAuthenticate     = "WWW-Authenticate"
@@ -214,6 +219,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 +247,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.6.1"
+	Version = "4.6.3"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
@@ -301,6 +307,12 @@ var (
 	}
 
 	MethodNotAllowedHandler = func(c Context) error {
+		// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)
+		// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned
+		routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)
+		if ok && routerAllowMethods != "" {
+			c.Response().Header().Set(HeaderAllow, routerAllowMethods)
+		}
 		return ErrMethodNotAllowed
 	}
 )
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/cors.go b/middleware/cors.go
index d6ef896..1625951 100644
--- a/middleware/cors.go
+++ b/middleware/cors.go
@@ -29,6 +29,8 @@ type (
 		// AllowMethods defines a list methods allowed when accessing the resource.
 		// This is used in response to a preflight request.
 		// Optional. Default value DefaultCORSConfig.AllowMethods.
+		// If `allowMethods` is left empty will fill for preflight request `Access-Control-Allow-Methods` header value
+		// from `Allow` header that echo.Router set into context.
 		AllowMethods []string `yaml:"allow_methods"`
 
 		// AllowHeaders defines a list of request headers that can be used when
@@ -41,6 +43,8 @@ type (
 		// a response to a preflight request, this indicates whether or not the
 		// actual request can be made using credentials.
 		// Optional. Default value false.
+		// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.
+		// See http://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
 		AllowCredentials bool `yaml:"allow_credentials"`
 
 		// ExposeHeaders defines a whitelist headers that clients are allowed to
@@ -80,7 +84,9 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 	if len(config.AllowOrigins) == 0 {
 		config.AllowOrigins = DefaultCORSConfig.AllowOrigins
 	}
+	hasCustomAllowMethods := true
 	if len(config.AllowMethods) == 0 {
+		hasCustomAllowMethods = false
 		config.AllowMethods = DefaultCORSConfig.AllowMethods
 	}
 
@@ -109,10 +115,28 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 			origin := req.Header.Get(echo.HeaderOrigin)
 			allowOrigin := ""
 
-			preflight := req.Method == http.MethodOptions
 			res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
 
-			// No Origin provided
+			// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
+			// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
+			// For simplicity we just consider method type and later `Origin` header.
+			preflight := req.Method == http.MethodOptions
+
+			// Although router adds special handler in case of OPTIONS method we avoid calling next for OPTIONS in this middleware
+			// as CORS requests do not have cookies / authentication headers by default, so we could get stuck in auth
+			// middlewares by calling next(c).
+			// But we still want to send `Allow` header as response in case of Non-CORS OPTIONS request as router default
+			// handler does.
+			routerAllowMethods := ""
+			if preflight {
+				tmpAllowMethods, ok := c.Get(echo.ContextKeyHeaderAllow).(string)
+				if ok && tmpAllowMethods != "" {
+					routerAllowMethods = tmpAllowMethods
+					c.Response().Header().Set(echo.HeaderAllow, routerAllowMethods)
+				}
+			}
+
+			// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
 			if origin == "" {
 				if !preflight {
 					return next(c)
@@ -145,19 +169,15 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 					}
 				}
 
-				// Check allowed origin patterns
-				for _, re := range allowOriginPatterns {
-					if allowOrigin == "" {
-						didx := strings.Index(origin, "://")
-						if didx == -1 {
-							continue
-						}
-						domAuth := origin[didx+3:]
-						// to avoid regex cost by invalid long domain
-						if len(domAuth) > 253 {
-							break
-						}
-
+				checkPatterns := false
+				if allowOrigin == "" {
+					// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
+					if len(origin) <= (253+3+5) && strings.Contains(origin, "://") {
+						checkPatterns = true
+					}
+				}
+				if checkPatterns {
+					for _, re := range allowOriginPatterns {
 						if match, _ := regexp.MatchString(re, origin); match {
 							allowOrigin = origin
 							break
@@ -174,12 +194,13 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 				return c.NoContent(http.StatusNoContent)
 			}
 
+			res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
+			if config.AllowCredentials {
+				res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
+			}
+
 			// Simple request
 			if !preflight {
-				res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
-				if config.AllowCredentials {
-					res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
-				}
 				if exposeHeaders != "" {
 					res.Header().Set(echo.HeaderAccessControlExposeHeaders, exposeHeaders)
 				}
@@ -189,11 +210,13 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 			// Preflight request
 			res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod)
 			res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders)
-			res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
-			res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods)
-			if config.AllowCredentials {
-				res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
+
+			if !hasCustomAllowMethods && routerAllowMethods != "" {
+				res.Header().Set(echo.HeaderAccessControlAllowMethods, routerAllowMethods)
+			} else {
+				res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods)
 			}
+
 			if allowHeaders != "" {
 				res.Header().Set(echo.HeaderAccessControlAllowHeaders, allowHeaders)
 			} else {
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 21e33ab..43605e3 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -68,9 +68,14 @@ type (
 		// - "form:<name>"
 		// Multiply sources example:
 		// - "header: Authorization,cookie: myowncookie"
-
 		TokenLookup string
 
+		// TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context.
+		// This is one of the two options to provide a token extractor.
+		// The order of precedence is user-defined TokenLookupFuncs, and TokenLookup.
+		// You can also provide both if you want.
+		TokenLookupFuncs []TokenLookupFunc
+
 		// AuthScheme to be used in the Authorization header.
 		// Optional. Default value "Bearer".
 		AuthScheme string
@@ -103,7 +108,8 @@ type (
 	// JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context.
 	JWTErrorHandlerWithContext func(error, echo.Context) error
 
-	jwtExtractor func(echo.Context) (string, error)
+	// TokenLookupFunc defines a function for extracting JWT token from the given context.
+	TokenLookupFunc func(echo.Context) (string, error)
 )
 
 // Algorithms
@@ -120,13 +126,14 @@ var (
 var (
 	// DefaultJWTConfig is the default JWT auth middleware config.
 	DefaultJWTConfig = JWTConfig{
-		Skipper:       DefaultSkipper,
-		SigningMethod: AlgorithmHS256,
-		ContextKey:    "user",
-		TokenLookup:   "header:" + echo.HeaderAuthorization,
-		AuthScheme:    "Bearer",
-		Claims:        jwt.MapClaims{},
-		KeyFunc:       nil,
+		Skipper:          DefaultSkipper,
+		SigningMethod:    AlgorithmHS256,
+		ContextKey:       "user",
+		TokenLookup:      "header:" + echo.HeaderAuthorization,
+		TokenLookupFuncs: nil,
+		AuthScheme:       "Bearer",
+		Claims:           jwt.MapClaims{},
+		KeyFunc:          nil,
 	}
 )
 
@@ -163,7 +170,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 	if config.Claims == nil {
 		config.Claims = DefaultJWTConfig.Claims
 	}
-	if config.TokenLookup == "" {
+	if config.TokenLookup == "" && len(config.TokenLookupFuncs) == 0 {
 		config.TokenLookup = DefaultJWTConfig.TokenLookup
 	}
 	if config.AuthScheme == "" {
@@ -179,7 +186,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 	// Initialize
 	// Split sources
 	sources := strings.Split(config.TokenLookup, ",")
-	var extractors []jwtExtractor
+	var extractors = config.TokenLookupFuncs
 	for _, source := range sources {
 		parts := strings.Split(source, ":")
 
@@ -290,8 +297,8 @@ func (config *JWTConfig) defaultKeyFunc(t *jwt.Token) (interface{}, error) {
 	return config.SigningKey, nil
 }
 
-// jwtFromHeader returns a `jwtExtractor` that extracts token from the request header.
-func jwtFromHeader(header string, authScheme string) jwtExtractor {
+// jwtFromHeader returns a `TokenLookupFunc` that extracts token from the request header.
+func jwtFromHeader(header string, authScheme string) TokenLookupFunc {
 	return func(c echo.Context) (string, error) {
 		auth := c.Request().Header.Get(header)
 		l := len(authScheme)
@@ -302,8 +309,8 @@ func jwtFromHeader(header string, authScheme string) jwtExtractor {
 	}
 }
 
-// jwtFromQuery returns a `jwtExtractor` that extracts token from the query string.
-func jwtFromQuery(param string) jwtExtractor {
+// jwtFromQuery returns a `TokenLookupFunc` that extracts token from the query string.
+func jwtFromQuery(param string) TokenLookupFunc {
 	return func(c echo.Context) (string, error) {
 		token := c.QueryParam(param)
 		if token == "" {
@@ -313,8 +320,8 @@ func jwtFromQuery(param string) jwtExtractor {
 	}
 }
 
-// jwtFromParam returns a `jwtExtractor` that extracts token from the url param string.
-func jwtFromParam(param string) jwtExtractor {
+// jwtFromParam returns a `TokenLookupFunc` that extracts token from the url param string.
+func jwtFromParam(param string) TokenLookupFunc {
 	return func(c echo.Context) (string, error) {
 		token := c.Param(param)
 		if token == "" {
@@ -324,8 +331,8 @@ func jwtFromParam(param string) jwtExtractor {
 	}
 }
 
-// jwtFromCookie returns a `jwtExtractor` that extracts token from the named cookie.
-func jwtFromCookie(name string) jwtExtractor {
+// jwtFromCookie returns a `TokenLookupFunc` that extracts token from the named cookie.
+func jwtFromCookie(name string) TokenLookupFunc {
 	return func(c echo.Context) (string, error) {
 		cookie, err := c.Cookie(name)
 		if err != nil {
@@ -335,8 +342,8 @@ func jwtFromCookie(name string) jwtExtractor {
 	}
 }
 
-// jwtFromForm returns a `jwtExtractor` that extracts token from the form field.
-func jwtFromForm(name string) jwtExtractor {
+// jwtFromForm returns a `TokenLookupFunc` that extracts token from the form field.
+func jwtFromForm(name string) TokenLookupFunc {
 	return func(c echo.Context) (string, error) {
 		field := c.FormValue(name)
 		if field == "" {
diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go
index edcc56a..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):
@@ -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/router.go b/router.go
index a8277c8..1a2ce56 100644
--- a/router.go
+++ b/router.go
@@ -1,6 +1,7 @@
 package echo
 
 import (
+	"bytes"
 	"net/http"
 )
 
@@ -31,17 +32,18 @@ type (
 	kind          uint8
 	children      []*node
 	methodHandler struct {
-		connect  HandlerFunc
-		delete   HandlerFunc
-		get      HandlerFunc
-		head     HandlerFunc
-		options  HandlerFunc
-		patch    HandlerFunc
-		post     HandlerFunc
-		propfind HandlerFunc
-		put      HandlerFunc
-		trace    HandlerFunc
-		report   HandlerFunc
+		connect     HandlerFunc
+		delete      HandlerFunc
+		get         HandlerFunc
+		head        HandlerFunc
+		options     HandlerFunc
+		patch       HandlerFunc
+		post        HandlerFunc
+		propfind    HandlerFunc
+		put         HandlerFunc
+		trace       HandlerFunc
+		report      HandlerFunc
+		allowHeader string
 	}
 )
 
@@ -68,6 +70,51 @@ func (m *methodHandler) isHandler() bool {
 		m.report != nil
 }
 
+func (m *methodHandler) updateAllowHeader() {
+	buf := new(bytes.Buffer)
+	buf.WriteString(http.MethodOptions)
+
+	if m.connect != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodConnect)
+	}
+	if m.delete != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodDelete)
+	}
+	if m.get != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodGet)
+	}
+	if m.head != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodHead)
+	}
+	if m.patch != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodPatch)
+	}
+	if m.post != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodPost)
+	}
+	if m.propfind != nil {
+		buf.WriteString(", PROPFIND")
+	}
+	if m.put != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodPut)
+	}
+	if m.trace != nil {
+		buf.WriteString(", ")
+		buf.WriteString(http.MethodTrace)
+	}
+	if m.report != nil {
+		buf.WriteString(", REPORT")
+	}
+	m.allowHeader = buf.String()
+}
+
 // NewRouter returns a new Router instance.
 func NewRouter(e *Echo) *Router {
 	return &Router{
@@ -99,6 +146,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] == '\\' {
+				path = path[:i-1] + path[i:]
+				i--
+				lcpIndex--
 				continue
 			}
 			j := i + 1
@@ -323,6 +373,7 @@ func (n *node) addHandler(method string, h HandlerFunc) {
 		n.methodHandler.report = h
 	}
 
+	n.methodHandler.updateAllowHeader()
 	if h != nil {
 		n.isHandler = true
 	} else {
@@ -359,13 +410,14 @@ func (n *node) findHandler(method string) HandlerFunc {
 	}
 }
 
-func (n *node) checkMethodNotAllowed() HandlerFunc {
-	for _, m := range methods {
-		if h := n.findHandler(m); h != nil {
-			return MethodNotAllowedHandler
-		}
+func optionsMethodHandler(allowMethods string) func(c Context) error {
+	return func(c Context) error {
+		// Note: we are not handling most of the CORS headers here. CORS is handled by CORS middleware
+		// 'OPTIONS' method RFC: https://httpwg.org/specs/rfc7231.html#OPTIONS
+		// 'Allow' header RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
+		c.Response().Header().Add(HeaderAllow, allowMethods)
+		return c.NoContent(http.StatusNoContent)
 	}
-	return NotFoundHandler
 }
 
 // Find lookup a handler registered for method and path. It also parses URL for path
@@ -560,7 +612,15 @@ func (r *Router) Find(method, path string, c Context) {
 		// use previous match as basis. although we have no matching handler we have path match.
 		// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)
 		currentNode = previousBestMatchNode
-		ctx.handler = currentNode.checkMethodNotAllowed()
+
+		ctx.handler = NotFoundHandler
+		if currentNode.isHandler {
+			ctx.Set(ContextKeyHeaderAllow, currentNode.methodHandler.allowHeader)
+			ctx.handler = MethodNotAllowedHandler
+			if method == http.MethodOptions {
+				ctx.handler = optionsMethodHandler(currentNode.methodHandler.allowHeader)
+			}
+		}
 	}
 	ctx.path = currentNode.ppath
 	ctx.pnames = currentNode.pnames


Test patch:
diff --git a/echo_test.go b/echo_test.go
index f289158..13a51b6 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -716,13 +716,16 @@ func TestEchoNotFound(t *testing.T) {
 
 func TestEchoMethodNotAllowed(t *testing.T) {
 	e := New()
+
 	e.GET("/", func(c Context) error {
 		return c.String(http.StatusOK, "Echo!")
 	})
 	req := httptest.NewRequest(http.MethodPost, "/", nil)
 	rec := httptest.NewRecorder()
 	e.ServeHTTP(rec, req)
+
 	assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
+	assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow))
 }
 
 func TestEchoContext(t *testing.T) {
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/cors_test.go b/middleware/cors_test.go
index 717abe4..daadbab 100644
--- a/middleware/cors_test.go
+++ b/middleware/cors_test.go
@@ -251,114 +251,238 @@ func Test_allowOriginSubdomain(t *testing.T) {
 	}
 }
 
+func TestCORSWithConfig_AllowMethods(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		allowOrigins    []string
+		allowContextKey string
+
+		whenOrigin       string
+		whenAllowMethods []string
+
+		expectAllow                     string
+		expectAccessControlAllowMethods string
+	}{
+		{
+			name:             "custom AllowMethods, preflight, no origin, sets only allow header from context key",
+			allowContextKey:  "OPTIONS, GET",
+			whenAllowMethods: []string{http.MethodGet, http.MethodHead},
+			whenOrigin:       "",
+			expectAllow:      "OPTIONS, GET",
+		},
+		{
+			name:             "default AllowMethods, preflight, no origin, no allow header in context key and in response",
+			allowContextKey:  "",
+			whenAllowMethods: nil,
+			whenOrigin:       "",
+			expectAllow:      "",
+		},
+		{
+			name:                            "custom AllowMethods, preflight, existing origin, sets both headers different values",
+			allowContextKey:                 "OPTIONS, GET",
+			whenAllowMethods:                []string{http.MethodGet, http.MethodHead},
+			whenOrigin:                      "http://google.com",
+			expectAllow:                     "OPTIONS, GET",
+			expectAccessControlAllowMethods: "GET,HEAD",
+		},
+		{
+			name:                            "default AllowMethods, preflight, existing origin, sets both headers",
+			allowContextKey:                 "OPTIONS, GET",
+			whenAllowMethods:                nil,
+			whenOrigin:                      "http://google.com",
+			expectAllow:                     "OPTIONS, GET",
+			expectAccessControlAllowMethods: "OPTIONS, GET",
+		},
+		{
+			name:                            "default AllowMethods, preflight, existing origin, no allows, sets only CORS allow methods",
+			allowContextKey:                 "",
+			whenAllowMethods:                nil,
+			whenOrigin:                      "http://google.com",
+			expectAllow:                     "",
+			expectAccessControlAllowMethods: "GET,HEAD,PUT,PATCH,POST,DELETE",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
+			e.GET("/test", func(c echo.Context) error {
+				return c.String(http.StatusOK, "OK")
+			})
+
+			cors := CORSWithConfig(CORSConfig{
+				AllowOrigins: tc.allowOrigins,
+				AllowMethods: tc.whenAllowMethods,
+			})
+
+			req := httptest.NewRequest(http.MethodOptions, "/test", nil)
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
+
+			req.Header.Set(echo.HeaderOrigin, tc.whenOrigin)
+			if tc.allowContextKey != "" {
+				c.Set(echo.ContextKeyHeaderAllow, tc.allowContextKey)
+			}
+
+			h := cors(echo.NotFoundHandler)
+			h(c)
+
+			assert.Equal(t, tc.expectAllow, rec.Header().Get(echo.HeaderAllow))
+			assert.Equal(t, tc.expectAccessControlAllowMethods, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
+		})
+	}
+}
+
 func TestCorsHeaders(t *testing.T) {
 	tests := []struct {
-		domain, allowedOrigin, method string
-		expected                      bool
+		name              string
+		originDomain      string
+		method            string
+		allowedOrigin     string
+		expected          bool
+		expectStatus      int
+		expectAllowHeader string
 	}{
 		{
-			domain:        "", // Request does not have Origin header
+			name:          "non-preflight request, allow any origin, missing origin header = no CORS logic done",
+			originDomain:  "",
 			allowedOrigin: "*",
 			method:        http.MethodGet,
 			expected:      false,
+			expectStatus:  http.StatusOK,
 		},
 		{
-			domain:        "http://example.com",
+			name:          "non-preflight request, allow any origin, specific origin domain",
+			originDomain:  "http://example.com",
 			allowedOrigin: "*",
 			method:        http.MethodGet,
 			expected:      true,
+			expectStatus:  http.StatusOK,
 		},
 		{
-			domain:        "", // Request does not have Origin header
+			name:          "non-preflight request, allow specific origin, missing origin header = no CORS logic done",
+			originDomain:  "", // Request does not have Origin header
 			allowedOrigin: "http://example.com",
 			method:        http.MethodGet,
 			expected:      false,
+			expectStatus:  http.StatusOK,
 		},
 		{
-			domain:        "http://bar.com",
+			name:          "non-preflight request, allow specific origin, different origin header = CORS logic failure",
+			originDomain:  "http://bar.com",
 			allowedOrigin: "http://example.com",
 			method:        http.MethodGet,
 			expected:      false,
+			expectStatus:  http.StatusOK,
 		},
 		{
-			domain:        "http://example.com",
+			name:          "non-preflight request, allow specific origin, matching origin header = CORS logic done",
+			originDomain:  "http://example.com",
 			allowedOrigin: "http://example.com",
 			method:        http.MethodGet,
 			expected:      true,
+			expectStatus:  http.StatusOK,
 		},
 		{
-			domain:        "", // Request does not have Origin header
-			allowedOrigin: "*",
-			method:        http.MethodOptions,
-			expected:      false,
+			name:              "preflight, allow any origin, missing origin header = no CORS logic done",
+			originDomain:      "", // Request does not have Origin header
+			allowedOrigin:     "*",
+			method:            http.MethodOptions,
+			expected:          false,
+			expectStatus:      http.StatusNoContent,
+			expectAllowHeader: "OPTIONS, GET, POST",
 		},
 		{
-			domain:        "http://example.com",
-			allowedOrigin: "*",
-			method:        http.MethodOptions,
-			expected:      true,
+			name:              "preflight, allow any origin, existing origin header = CORS logic done",
+			originDomain:      "http://example.com",
+			allowedOrigin:     "*",
+			method:            http.MethodOptions,
+			expected:          true,
+			expectStatus:      http.StatusNoContent,
+			expectAllowHeader: "OPTIONS, GET, POST",
 		},
 		{
-			domain:        "", // Request does not have Origin header
-			allowedOrigin: "http://example.com",
-			method:        http.MethodOptions,
-			expected:      false,
+			name:              "preflight, allow any origin, missing origin header = no CORS logic done",
+			originDomain:      "", // Request does not have Origin header
+			allowedOrigin:     "http://example.com",
+			method:            http.MethodOptions,
+			expected:          false,
+			expectStatus:      http.StatusNoContent,
+			expectAllowHeader: "OPTIONS, GET, POST",
 		},
 		{
-			domain:        "http://bar.com",
-			allowedOrigin: "http://example.com",
-			method:        http.MethodGet,
-			expected:      false,
+			name:              "preflight, allow specific origin, different origin header = no CORS logic done",
+			originDomain:      "http://bar.com",
+			allowedOrigin:     "http://example.com",
+			method:            http.MethodOptions,
+			expected:          false,
+			expectStatus:      http.StatusNoContent,
+			expectAllowHeader: "OPTIONS, GET, POST",
 		},
 		{
-			domain:        "http://example.com",
-			allowedOrigin: "http://example.com",
-			method:        http.MethodOptions,
-			expected:      true,
+			name:              "preflight, allow specific origin, matching origin header = CORS logic done",
+			originDomain:      "http://example.com",
+			allowedOrigin:     "http://example.com",
+			method:            http.MethodOptions,
+			expected:          true,
+			expectStatus:      http.StatusNoContent,
+			expectAllowHeader: "OPTIONS, GET, POST",
 		},
 	}
 
-	e := echo.New()
-	for _, tt := range tests {
-		req := httptest.NewRequest(tt.method, "/", nil)
-		rec := httptest.NewRecorder()
-		c := e.NewContext(req, rec)
-		if tt.domain != "" {
-			req.Header.Set(echo.HeaderOrigin, tt.domain)
-		}
-		cors := CORSWithConfig(CORSConfig{
-			AllowOrigins: []string{tt.allowedOrigin},
-			//AllowCredentials: true,
-			//MaxAge:           3600,
-		})
-		h := cors(echo.NotFoundHandler)
-		h(c)
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
 
-		assert.Equal(t, echo.HeaderOrigin, rec.Header().Get(echo.HeaderVary))
+			e.Use(CORSWithConfig(CORSConfig{
+				AllowOrigins: []string{tc.allowedOrigin},
+				//AllowCredentials: true,
+				//MaxAge:           3600,
+			}))
 
-		expectedAllowOrigin := ""
-		if tt.allowedOrigin == "*" {
-			expectedAllowOrigin = "*"
-		} else {
-			expectedAllowOrigin = tt.domain
-		}
+			e.GET("/", func(c echo.Context) error {
+				return c.String(http.StatusOK, "OK")
+			})
+			e.POST("/", func(c echo.Context) error {
+				return c.String(http.StatusCreated, "OK")
+			})
 
-		switch {
-		case tt.expected && tt.method == http.MethodOptions:
-			assert.Contains(t, rec.Header(), echo.HeaderAccessControlAllowMethods)
-			assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-			assert.Equal(t, 3, len(rec.Header()[echo.HeaderVary]))
-		case tt.expected && tt.method == http.MethodGet:
-			assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-			assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin
-		default:
-			assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
-			assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin
-		}
+			req := httptest.NewRequest(tc.method, "/", nil)
+			rec := httptest.NewRecorder()
+
+			if tc.originDomain != "" {
+				req.Header.Set(echo.HeaderOrigin, tc.originDomain)
+			}
+
+			// we run through whole Echo handler chain to see how CORS works with Router OPTIONS handler
+			e.ServeHTTP(rec, req)
+
+			assert.Equal(t, echo.HeaderOrigin, rec.Header().Get(echo.HeaderVary))
+			assert.Equal(t, tc.expectAllowHeader, rec.Header().Get(echo.HeaderAllow))
+			assert.Equal(t, tc.expectStatus, rec.Code)
+
+			expectedAllowOrigin := ""
+			if tc.allowedOrigin == "*" {
+				expectedAllowOrigin = "*"
+			} else {
+				expectedAllowOrigin = tc.originDomain
+			}
+			switch {
+			case tc.expected && tc.method == http.MethodOptions:
+				assert.Contains(t, rec.Header(), echo.HeaderAccessControlAllowMethods)
+				assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
+
+				assert.Equal(t, 3, len(rec.Header()[echo.HeaderVary]))
+
+			case tc.expected && tc.method == http.MethodGet:
+				assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
+				assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin
+			default:
+				assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
+				assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin
+			}
+		})
 
-		if tt.method == http.MethodOptions {
-			assert.Equal(t, http.StatusNoContent, rec.Code)
-		}
 	}
 }
 
diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go
index 5f36ce0..18454d0 100644
--- a/middleware/jwt_test.go
+++ b/middleware/jwt_test.go
@@ -603,3 +603,27 @@ func TestJWTConfig_custom_ParseTokenFunc_Keyfunc(t *testing.T) {
 
 	assert.Equal(t, http.StatusTeapot, res.Code)
 }
+
+func TestJWTConfig_TokenLookupFuncs(t *testing.T) {
+	e := echo.New()
+
+	e.GET("/", func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	e.Use(JWTWithConfig(JWTConfig{
+		TokenLookupFuncs: []TokenLookupFunc{
+			func(c echo.Context) (string, error) {
+				return c.Request().Header.Get("X-API-Key"), nil
+			},
+		},
+		SigningKey: []byte("secret"),
+	}))
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set("X-API-Key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
+	res := httptest.NewRecorder()
+	e.ServeHTTP(res, req)
+
+	assert.Equal(t, http.StatusOK, res.Code)
+}
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/router_test.go b/router_test.go
index 1cb36b4..5cbb8d9 100644
--- a/router_test.go
+++ b/router_test.go
@@ -3,6 +3,7 @@ package echo
 import (
 	"fmt"
 	"net/http"
+	"net/http/httptest"
 	"strings"
 	"testing"
 
@@ -725,12 +726,13 @@ func TestMethodNotAllowedAndNotFound(t *testing.T) {
 	r.Add(http.MethodPost, "/users/:id", handlerFunc)
 
 	var testCases = []struct {
-		name        string
-		whenMethod  string
-		whenURL     string
-		expectRoute interface{}
-		expectParam map[string]string
-		expectError error
+		name              string
+		whenMethod        string
+		whenURL           string
+		expectRoute       interface{}
+		expectParam       map[string]string
+		expectError       error
+		expectAllowHeader string
 	}{
 		{
 			name:        "exact match for route+method",
@@ -740,11 +742,12 @@ func TestMethodNotAllowedAndNotFound(t *testing.T) {
 			expectParam: map[string]string{"id": "1"},
 		},
 		{
-			name:        "matches node but not method. sends 405 from best match node",
-			whenMethod:  http.MethodPut,
-			whenURL:     "/users/1",
-			expectRoute: nil,
-			expectError: ErrMethodNotAllowed,
+			name:              "matches node but not method. sends 405 from best match node",
+			whenMethod:        http.MethodPut,
+			whenURL:           "/users/1",
+			expectRoute:       nil,
+			expectError:       ErrMethodNotAllowed,
+			expectAllowHeader: "OPTIONS, POST",
 		},
 		{
 			name:        "best match is any route up in tree",
@@ -756,7 +759,9 @@ func TestMethodNotAllowedAndNotFound(t *testing.T) {
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			c := e.NewContext(nil, nil).(*context)
+			req := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec).(*context)
 
 			method := http.MethodGet
 			if tc.whenMethod != "" {
@@ -775,10 +780,36 @@ func TestMethodNotAllowedAndNotFound(t *testing.T) {
 				assert.Equal(t, expectedValue, c.Param(param))
 			}
 			checkUnusedParamValues(t, c, tc.expectParam)
+
+			assert.Equal(t, tc.expectAllowHeader, c.Response().Header().Get(HeaderAllow))
 		})
 	}
 }
 
+func TestRouterOptionsMethodHandler(t *testing.T) {
+	e := New()
+
+	var keyInContext interface{}
+	e.Use(func(next HandlerFunc) HandlerFunc {
+		return func(c Context) error {
+			err := next(c)
+			keyInContext = c.Get(ContextKeyHeaderAllow)
+			return err
+		}
+	})
+	e.GET("/test", func(c Context) error {
+		return c.String(http.StatusOK, "Echo!")
+	})
+
+	req := httptest.NewRequest(http.MethodOptions, "/test", nil)
+	rec := httptest.NewRecorder()
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusNoContent, rec.Code)
+	assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow))
+	assert.Equal(t, "OPTIONS, GET", keyInContext)
+}
+
 func TestRouterTwoParam(t *testing.T) {
 	e := New()
 	r := e.router
@@ -1124,6 +1155,8 @@ func TestRouterParam_escapeColon(t *testing.T) {
 	e := New()
 
 	e.POST("/files/a/long/file\\:undelete", handlerFunc)
+	e.POST("/multilevel\\:undelete/second\\:something", handlerFunc)
+	e.POST("/mixed/:id/second\\:something", handlerFunc)
 	e.POST("/v1/some/resource/name:customVerb", handlerFunc)
 
 	var testCases = []struct {
@@ -1133,12 +1166,22 @@ func TestRouterParam_escapeColon(t *testing.T) {
 		expectError string
 	}{
 		{
-			whenURL:     "/files/a/long/file\\:undelete",
+			whenURL:     "/files/a/long/file:undelete",
 			expectRoute: "/files/a/long/file\\:undelete",
 			expectParam: map[string]string{},
 		},
 		{
-			whenURL:     "/files/a/long/file\\:notMatching",
+			whenURL:     "/multilevel:undelete/second:something",
+			expectRoute: "/multilevel\\:undelete/second\\:something",
+			expectParam: map[string]string{},
+		},
+		{
+			whenURL:     "/mixed/123/second:something",
+			expectRoute: "/mixed/:id/second\\:something",
+			expectParam: map[string]string{"id": "123"},
+		},
+		{
+			whenURL:     "/files/a/long/file:notMatching",
 			expectRoute: nil,
 			expectError: "code=404, message=Not Found",
 			expectParam: nil,
@@ -2276,6 +2319,73 @@ func TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {
 	}
 }
 
+func TestRouterHandleMethodOptions(t *testing.T) {
+	e := New()
+	r := e.router
+
+	r.Add(http.MethodGet, "/users", handlerFunc)
+	r.Add(http.MethodPost, "/users", handlerFunc)
+	r.Add(http.MethodPut, "/users/:id", handlerFunc)
+	r.Add(http.MethodGet, "/users/:id", handlerFunc)
+
+	var testCases = []struct {
+		name              string
+		whenMethod        string
+		whenURL           string
+		expectAllowHeader string
+		expectStatus      int
+	}{
+		{
+			name:              "allows GET and POST handlers",
+			whenMethod:        http.MethodOptions,
+			whenURL:           "/users",
+			expectAllowHeader: "OPTIONS, GET, POST",
+			expectStatus:      http.StatusNoContent,
+		},
+		{
+			name:              "allows GET and PUT handlers",
+			whenMethod:        http.MethodOptions,
+			whenURL:           "/users/1",
+			expectAllowHeader: "OPTIONS, GET, PUT",
+			expectStatus:      http.StatusNoContent,
+		},
+		{
+			name:              "GET does not have allows header",
+			whenMethod:        http.MethodGet,
+			whenURL:           "/users",
+			expectAllowHeader: "",
+			expectStatus:      http.StatusOK,
+		},
+		{
+			name:              "path with no handlers does not set Allows header",
+			whenMethod:        http.MethodOptions,
+			whenURL:           "/notFound",
+			expectAllowHeader: "",
+			expectStatus:      http.StatusNotFound,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			req := httptest.NewRequest(tc.whenMethod, tc.whenURL, nil)
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec).(*context)
+
+			r.Find(tc.whenMethod, tc.whenURL, c)
+			err := c.handler(c)
+
+			if tc.expectStatus >= 400 {
+				assert.Error(t, err)
+				he := err.(*HTTPError)
+				assert.Equal(t, tc.expectStatus, he.Code)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.expectStatus, rec.Code)
+			}
+			assert.Equal(t, tc.expectAllowHeader, c.Response().Header().Get("Allow"))
+		})
+	}
+}
+
 func benchmarkRouterRoutes(b *testing.B, routes []*Route, routesToFind []*Route) {
 	e := New()
 	r := e.router