Repository: labstack/echo
Pull request #2341 (2022-11-21 09:55:40)

Additional configuration options for RequestLogger and Logger middleware
* Add `middleware.RequestLoggerConfig.HandleError` configuration option to handle error within middleware with global error handler thus setting response status code decided by error handler and not derived from error itself.
* Add `middleware.LoggerConfig.CustomTagFunc` so Logger middleware can add custom text/fields etc to logged (JSON or whatever format) row.

```go
	e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
		Format: `{"method":"${method}",${custom}}` + "\n",
		CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) {
			return buf.WriteString(`"tag":"my-value"`)
		},
	}))
```

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/2341?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
Base: **92.38**% // Head: **92.33**% // Decreases project coverage by **`-0.05%`** :warning:
> Coverage data is based on head [(`b26f284`)](https://codecov.io/gh/labstack/echo/pull/2341?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) compared to base [(`be23ab6`)](https://codecov.io/gh/labstack/echo/commit/be23ab67ccdcb94ff99f49638b252be03ae274a6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).
> Patch coverage: 72.72% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2341      +/-   ##
==========================================
- Coverage   92.38%   92.33%   -0.06%     
==========================================
  Files          37       37              
  Lines        4454     4463       +9     
==========================================
+ Hits         4115     4121       +6     
- Misses        247      249       +2     
- Partials       92       93       +1     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/2341?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) | Coverage Δ | |
|---|---|---|
| [context.go](https://codecov.io/gh/labstack/echo/pull/2341/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-Y29udGV4dC5nbw==) | `88.69% <ø> (ø)` | |
| [echo.go](https://codecov.io/gh/labstack/echo/pull/2341/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-ZWNoby5nbw==) | `95.24% <ø> (ø)` | |
| [middleware/logger.go](https://codecov.io/gh/labstack/echo/pull/2341/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-bWlkZGxld2FyZS9sb2dnZXIuZ28=) | `84.89% <40.00%> (-1.68%)` | :arrow_down: |
| [middleware/request\_logger.go](https://codecov.io/gh/labstack/echo/pull/2341/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack#diff-bWlkZGxld2FyZS9yZXF1ZXN0X2xvZ2dlci5nbw==) | `97.56% <100.00%> (+0.08%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/labstack/echo/pull/2341?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).

aldas: @vishr or @lammel do you have time? If not, I'll merge this in couple of days :)

Patch:
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index c2bd41e..e35e7f1 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -44,13 +44,19 @@ jobs:
         with:
           go-version: ${{ matrix.go }}
 
-      - name: Install Dependencies
-        run: go install golang.org/x/lint/golint@latest
-
       - name: Run Tests
+        run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
+
+      - name: Install dependencies for checks
         run: |
-          golint -set_exit_status ./...
-          go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
+          go install golang.org/x/lint/golint@latest
+          go install honnef.co/go/tools/cmd/staticcheck@latest
+
+      - name: Run golint
+        run: golint -set_exit_status ./...
+
+      - name: Run staticcheck
+        run: staticcheck ./...
 
       - name: Upload coverage to Codecov
         if: success() && matrix.go == 1.19 && matrix.os == 'ubuntu-latest'
diff --git a/Makefile b/Makefile
index 3b76519..6aff6a8 100644
--- a/Makefile
+++ b/Makefile
@@ -10,8 +10,10 @@ check: lint vet race ## Check project
 
 init:
 	@go install golang.org/x/lint/golint@latest
+	@go install honnef.co/go/tools/cmd/staticcheck@latest
 
 lint: ## Lint the files
+	@staticcheck ${PKG_LIST}
 	@golint -set_exit_status ${PKG_LIST}
 
 vet: ## Vet the files
diff --git a/context.go b/context.go
index 5567100..b3a7ce8 100644
--- a/context.go
+++ b/context.go
@@ -169,7 +169,11 @@ type (
 		// Redirect redirects the request to a provided URL with status code.
 		Redirect(code int, url string) error
 
-		// Error invokes the registered HTTP error handler. Generally used by middleware.
+		// Error invokes the registered global HTTP error handler. Generally used by middleware.
+		// A side-effect of calling global error handler is that now Response has been committed (sent to the client) and
+		// middlewares up in chain can not change Response status code or Response body anymore.
+		//
+		// Avoid using this method in handlers as no middleware will be able to effectively handle errors after that.
 		Error(err error)
 
 		// Handler returns the matched handler by router.
@@ -282,11 +286,16 @@ func (c *context) RealIP() string {
 	if ip := c.request.Header.Get(HeaderXForwardedFor); ip != "" {
 		i := strings.IndexAny(ip, ",")
 		if i > 0 {
-			return strings.TrimSpace(ip[:i])
+			xffip := strings.TrimSpace(ip[:i])
+			xffip = strings.TrimPrefix(xffip, "[")
+			xffip = strings.TrimSuffix(xffip, "]")
+			return xffip
 		}
 		return ip
 	}
 	if ip := c.request.Header.Get(HeaderXRealIP); ip != "" {
+		ip = strings.TrimPrefix(ip, "[")
+		ip = strings.TrimSuffix(ip, "]")
 		return ip
 	}
 	ra, _, _ := net.SplitHostPort(c.request.RemoteAddr)
diff --git a/echo.go b/echo.go
index 28f6565..2f54d77 100644
--- a/echo.go
+++ b/echo.go
@@ -43,10 +43,10 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	stdLog "log"
 	"net"
 	"net/http"
+	"os"
 	"reflect"
 	"runtime"
 	"sync"
@@ -116,7 +116,7 @@ type (
 	HandlerFunc func(c Context) error
 
 	// HTTPErrorHandler is a centralized HTTP error handler.
-	HTTPErrorHandler func(error, Context)
+	HTTPErrorHandler func(err error, c Context)
 
 	// Validator is the interface that wraps the Validate function.
 	Validator interface {
@@ -700,7 +700,7 @@ func (e *Echo) StartTLS(address string, certFile, keyFile interface{}) (err erro
 func filepathOrContent(fileOrContent interface{}) (content []byte, err error) {
 	switch v := fileOrContent.(type) {
 	case string:
-		return ioutil.ReadFile(v)
+		return os.ReadFile(v)
 	case []byte:
 		return v, nil
 	default:
diff --git a/echo_fs.go b/echo_fs.go
index b8526da..9f83a03 100644
--- a/echo_fs.go
+++ b/echo_fs.go
@@ -7,7 +7,6 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
-	"runtime"
 	"strings"
 )
 
@@ -125,7 +124,7 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
 		// we need to make exception for `defaultFS` instances as it interprets root prefix differently from fs.FS.
 		// fs.Fs.Open does not like relative paths ("./", "../") and absolute paths at all but prior echo.Filesystem we
 		// were able to use paths like `./myfile.log`, `/etc/hosts` and these would work fine with `os.Open` but not with fs.Fs
-		if isRelativePath(root) {
+		if !filepath.IsAbs(root) {
 			root = filepath.Join(dFS.prefix, root)
 		}
 		return &defaultFS{
@@ -136,21 +135,6 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
 	return fs.Sub(currentFs, root)
 }
 
-func isRelativePath(path string) bool {
-	if path == "" {
-		return true
-	}
-	if path[0] == '/' {
-		return false
-	}
-	if runtime.GOOS == "windows" && strings.IndexByte(path, ':') != -1 {
-		// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#file_and_directory_names
-		// https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats
-		return false
-	}
-	return true
-}
-
 // MustSubFS creates sub FS from current filesystem or panic on failure.
 // Panic happens when `fsRoot` contains invalid path according to `fs.ValidPath` rules.
 //
diff --git a/ip.go b/ip.go
index 46d464c..1bcd756 100644
--- a/ip.go
+++ b/ip.go
@@ -227,6 +227,8 @@ func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
 	return func(req *http.Request) string {
 		realIP := req.Header.Get(HeaderXRealIP)
 		if realIP != "" {
+			realIP = strings.TrimPrefix(realIP, "[")
+			realIP = strings.TrimSuffix(realIP, "]")
 			if ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) {
 				return realIP
 			}
@@ -248,7 +250,10 @@ func ExtractIPFromXFFHeader(options ...TrustOption) IPExtractor {
 		}
 		ips := append(strings.Split(strings.Join(xffs, ","), ","), directIP)
 		for i := len(ips) - 1; i >= 0; i-- {
-			ip := net.ParseIP(strings.TrimSpace(ips[i]))
+			ips[i] = strings.TrimSpace(ips[i])
+			ips[i] = strings.TrimPrefix(ips[i], "[")
+			ips[i] = strings.TrimSuffix(ips[i], "]")
+			ip := net.ParseIP(ips[i])
 			if ip == nil {
 				// Unable to parse IP; cannot trust entire records
 				return directIP
diff --git a/middleware/body_dump.go b/middleware/body_dump.go
index ebd0d0a..fa7891b 100644
--- a/middleware/body_dump.go
+++ b/middleware/body_dump.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"bytes"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 
@@ -68,9 +67,9 @@ func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
 			// Request
 			reqBody := []byte{}
 			if c.Request().Body != nil { // Read
-				reqBody, _ = ioutil.ReadAll(c.Request().Body)
+				reqBody, _ = io.ReadAll(c.Request().Body)
 			}
-			c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
+			c.Request().Body = io.NopCloser(bytes.NewBuffer(reqBody)) // Reset
 
 			// Response
 			resBody := new(bytes.Buffer)
diff --git a/middleware/compress.go b/middleware/compress.go
index ac6672e..9e5f610 100644
--- a/middleware/compress.go
+++ b/middleware/compress.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"compress/gzip"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"strings"
@@ -89,7 +88,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 						// nothing is written to body or error is returned.
 						// See issue #424, #407.
 						res.Writer = rw
-						w.Reset(ioutil.Discard)
+						w.Reset(io.Discard)
 					}
 					w.Close()
 					pool.Put(w)
@@ -135,7 +134,7 @@ func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
 func gzipCompressPool(config GzipConfig) sync.Pool {
 	return sync.Pool{
 		New: func() interface{} {
-			w, err := gzip.NewWriterLevel(ioutil.Discard, config.Level)
+			w, err := gzip.NewWriterLevel(io.Discard, config.Level)
 			if err != nil {
 				return err
 			}
diff --git a/middleware/jwt.go b/middleware/jwt.go
index bec5167..ef6ad4e 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -262,7 +262,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 }
 
 func (config *JWTConfig) defaultParseToken(auth string, c echo.Context) (interface{}, error) {
-	token := new(jwt.Token)
+	var token *jwt.Token
 	var err error
 	// Issue #647, #656
 	if _, ok := config.Claims.(jwt.MapClaims); ok {
diff --git a/middleware/logger.go b/middleware/logger.go
index a21df8f..8bf335f 100644
--- a/middleware/logger.go
+++ b/middleware/logger.go
@@ -47,6 +47,7 @@ type (
 		// - header:<NAME>
 		// - query:<NAME>
 		// - form:<NAME>
+		// - custom (see CustomTagFunc field)
 		//
 		// Example "${remote_ip} ${status}"
 		//
@@ -56,6 +57,11 @@ type (
 		// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
 		CustomTimeFormat string `yaml:"custom_time_format"`
 
+		// CustomTagFunc is function called for `${custom}` tag to output user implemented text by writing it to buf.
+		// Make sure that outputted text creates valid JSON string with other logged tags.
+		// Optional.
+		CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error)
+
 		// Output is a writer where logs in JSON format are written.
 		// Optional. Default value os.Stdout.
 		Output io.Writer
@@ -126,6 +132,11 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
 
 			if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
 				switch tag {
+				case "custom":
+					if config.CustomTagFunc == nil {
+						return 0, nil
+					}
+					return config.CustomTagFunc(c, buf)
 				case "time_unix":
 					return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
 				case "time_unix_milli":
diff --git a/middleware/request_logger.go b/middleware/request_logger.go
index 7a4d982..b9e3692 100644
--- a/middleware/request_logger.go
+++ b/middleware/request_logger.go
@@ -10,10 +10,16 @@ import (
 
 // Example for `fmt.Printf`
 // 	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogStatus: true,
-//		LogURI:    true,
+//		LogStatus:   true,
+//		LogURI:      true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
 //		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
-//			fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
+//			if v.Error == nil {
+//				fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
+//			} else {
+//				fmt.Printf("REQUEST_ERROR: uri: %v, status: %v, err: %v\n", v.URI, v.Status, v.Error)
+//			}
 //			return nil
 //		},
 //	}))
@@ -21,14 +27,23 @@ import (
 // Example for Zerolog (https://github.com/rs/zerolog)
 // 	logger := zerolog.New(os.Stdout)
 //	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogURI:    true,
-//		LogStatus: true,
+//		LogURI:      true,
+//		LogStatus:   true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
 //		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
-//			logger.Info().
-//				Str("URI", v.URI).
-//				Int("status", v.Status).
-//				Msg("request")
-//
+//			if v.Error == nil {
+//				logger.Info().
+//					Str("URI", v.URI).
+//					Int("status", v.Status).
+//					Msg("request")
+//			} else {
+//				logger.Error().
+//					Err(v.Error).
+//					Str("URI", v.URI).
+//					Int("status", v.Status).
+//					Msg("request error")
+//			}
 //			return nil
 //		},
 //	}))
@@ -36,29 +51,47 @@ import (
 // Example for Zap (https://github.com/uber-go/zap)
 // 	logger, _ := zap.NewProduction()
 //	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogURI:    true,
-//		LogStatus: true,
+//		LogURI:      true,
+//		LogStatus:   true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
 //		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
-//			logger.Info("request",
-//				zap.String("URI", v.URI),
-//				zap.Int("status", v.Status),
-//			)
-//
+//			if v.Error == nil {
+//				logger.Info("request",
+//					zap.String("URI", v.URI),
+//					zap.Int("status", v.Status),
+//				)
+//			} else {
+//				logger.Error("request error",
+//					zap.String("URI", v.URI),
+//					zap.Int("status", v.Status),
+//					zap.Error(v.Error),
+//				)
+//			}
 //			return nil
 //		},
 //	}))
 //
 // Example for Logrus (https://github.com/sirupsen/logrus)
-// 	log := logrus.New()
+//  log := logrus.New()
 //	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
-//		LogURI:    true,
-//		LogStatus: true,
-//		LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error {
-//			log.WithFields(logrus.Fields{
-//				"URI":   values.URI,
-//				"status": values.Status,
-//			}).Info("request")
-//
+//		LogURI:      true,
+//		LogStatus:   true,
+//		LogError:    true,
+//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
+//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
+//			if v.Error == nil {
+//				log.WithFields(logrus.Fields{
+//					"URI":    v.URI,
+//					"status": v.Status,
+//				}).Info("request")
+//			} else {
+//				log.WithFields(logrus.Fields{
+//					"URI":    v.URI,
+//					"status": v.Status,
+//					"error":  v.Error,
+//				}).Error("request error")
+//			}
 //			return nil
 //		},
 //	}))
@@ -74,6 +107,13 @@ type RequestLoggerConfig struct {
 	// Mandatory.
 	LogValuesFunc func(c echo.Context, v RequestLoggerValues) error
 
+	// HandleError instructs logger to call global error handler when next middleware/handler returns an error.
+	// This is useful when you have custom error handler that can decide to use different status codes.
+	//
+	// A side-effect of calling global error handler is that now Response has been committed and sent to the client
+	// and middlewares up in chain can not change Response status code or response body.
+	HandleError bool
+
 	// LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call).
 	LogLatency bool
 	// LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
@@ -217,6 +257,9 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 				config.BeforeNextFunc(c)
 			}
 			err := next(c)
+			if config.HandleError {
+				c.Error(err)
+			}
 
 			v := RequestLoggerValues{
 				StartTime: start,
@@ -264,7 +307,9 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 			}
 			if config.LogStatus {
 				v.Status = res.Status
-				if err != nil {
+				if err != nil && !config.HandleError {
+					//  this block should not be executed in case of HandleError=true as the global error handler will decide
+					//  the status code. In that case status code could be different from what err contains.
 					var httpErr *echo.HTTPError
 					if errors.As(err, &httpErr) {
 						v.Status = httpErr.Code
@@ -310,6 +355,9 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 				return errOnLog
 			}
 
+			// in case of HandleError=true we are returning the error that we already have handled with global error handler
+			// this is deliberate as this error could be useful for upstream middlewares and default global error handler
+			// will ignore that error when it bubbles up in middleware chain.
 			return err
 		}
 	}, nil


Test patch:
diff --git a/context_test.go b/context_test.go
index b25e11c..11a63cf 100644
--- a/context_test.go
+++ b/context_test.go
@@ -97,7 +97,6 @@ func (responseWriterErr) Write([]byte) (int, error) {
 }
 
 func (responseWriterErr) WriteHeader(statusCode int) {
-
 }
 
 func TestContext(t *testing.T) {
@@ -904,6 +903,30 @@ func TestContext_RealIP(t *testing.T) {
 			},
 			"127.0.0.1",
 		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"[2001:db8:85a3:8d3:1319:8a2e:370:7348], 2001:db8::1, "}},
+				},
+			},
+			"2001:db8:85a3:8d3:1319:8a2e:370:7348",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"[2001:db8:85a3:8d3:1319:8a2e:370:7348],[2001:db8::1]"}},
+				},
+			},
+			"2001:db8:85a3:8d3:1319:8a2e:370:7348",
+		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{HeaderXForwardedFor: []string{"2001:db8:85a3:8d3:1319:8a2e:370:7348"}},
+				},
+			},
+			"2001:db8:85a3:8d3:1319:8a2e:370:7348",
+		},
 		{
 			&context{
 				request: &http.Request{
@@ -914,6 +937,17 @@ func TestContext_RealIP(t *testing.T) {
 			},
 			"192.168.0.1",
 		},
+		{
+			&context{
+				request: &http.Request{
+					Header: http.Header{
+						"X-Real-Ip": []string{"[2001:db8::1]"},
+					},
+				},
+			},
+			"2001:db8::1",
+		},
+
 		{
 			&context{
 				request: &http.Request{
diff --git a/echo_test.go b/echo_test.go
index aa2954c..b0d1ccd 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -244,7 +243,7 @@ func TestEchoStaticRedirectIndex(t *testing.T) {
 		}(resp.Body)
 		assert.Equal(t, http.StatusOK, resp.StatusCode)
 
-		if body, err := ioutil.ReadAll(resp.Body); err == nil {
+		if body, err := io.ReadAll(resp.Body); err == nil {
 			assert.Equal(t, true, strings.HasPrefix(string(body), "<!doctype html>"))
 		} else {
 			assert.Fail(t, err.Error())
@@ -1032,9 +1031,9 @@ func TestEchoStartTLSAndStart(t *testing.T) {
 }
 
 func TestEchoStartTLSByteString(t *testing.T) {
-	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	cert, err := os.ReadFile("_fixture/certs/cert.pem")
 	require.NoError(t, err)
-	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	key, err := os.ReadFile("_fixture/certs/key.pem")
 	require.NoError(t, err)
 
 	testCases := []struct {
@@ -1413,7 +1412,7 @@ func TestEchoListenerNetwork(t *testing.T) {
 				}(resp.Body)
 				assert.Equal(t, http.StatusOK, resp.StatusCode)
 
-				if body, err := ioutil.ReadAll(resp.Body); err == nil {
+				if body, err := io.ReadAll(resp.Body); err == nil {
 					assert.Equal(t, "OK", string(body))
 				} else {
 					assert.Fail(t, err.Error())
@@ -1495,9 +1494,9 @@ func TestEcho_ListenerAddr(t *testing.T) {
 }
 
 func TestEcho_TLSListenerAddr(t *testing.T) {
-	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	cert, err := os.ReadFile("_fixture/certs/cert.pem")
 	require.NoError(t, err)
-	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	key, err := os.ReadFile("_fixture/certs/key.pem")
 	require.NoError(t, err)
 
 	e := New()
@@ -1515,9 +1514,9 @@ func TestEcho_TLSListenerAddr(t *testing.T) {
 }
 
 func TestEcho_StartServer(t *testing.T) {
-	cert, err := ioutil.ReadFile("_fixture/certs/cert.pem")
+	cert, err := os.ReadFile("_fixture/certs/cert.pem")
 	require.NoError(t, err)
-	key, err := ioutil.ReadFile("_fixture/certs/key.pem")
+	key, err := os.ReadFile("_fixture/certs/key.pem")
 	require.NoError(t, err)
 	certs, err := tls.X509KeyPair(cert, key)
 	require.NoError(t, err)
diff --git a/group_test.go b/group_test.go
index 24f1916..01c304d 100644
--- a/group_test.go
+++ b/group_test.go
@@ -1,9 +1,9 @@
 package echo
 
 import (
-	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -32,7 +32,7 @@ func TestGroupFile(t *testing.T) {
 	e := New()
 	g := e.Group("/group")
 	g.File("/walle", "_fixture/images/walle.png")
-	expectedData, err := ioutil.ReadFile("_fixture/images/walle.png")
+	expectedData, err := os.ReadFile("_fixture/images/walle.png")
 	assert.Nil(t, err)
 	req := httptest.NewRequest(http.MethodGet, "/group/walle", nil)
 	rec := httptest.NewRecorder()
diff --git a/ip_test.go b/ip_test.go
index 755900d..38c4a1c 100644
--- a/ip_test.go
+++ b/ip_test.go
@@ -459,6 +459,7 @@ func TestExtractIPDirect(t *testing.T) {
 
 func TestExtractIPFromRealIPHeader(t *testing.T) {
 	_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR("203.0.113.199/24")
+	_, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR("2001:db8::/64")
 
 	var testCases = []struct {
 		name              string
@@ -493,6 +494,16 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.1",
 		},
+		{
+			name: "request is from external IP has valid + UNTRUSTED external X-Real-Ip header, extract IP from remote addr",
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXRealIP: []string{"[2001:db8::113:199]"}, // <-- this is untrusted
+				},
+				RemoteAddr: "[2001:db8::113:1]:8080",
+			},
+			expectIP: "2001:db8::113:1",
+		},
 		{
 			name: "request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
 			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
@@ -506,6 +517,19 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.199",
 		},
+		{
+			name: "request is from external IP has valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
+			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
+				TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64"
+			},
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXRealIP: []string{"[2001:db8::113:199]"},
+				},
+				RemoteAddr: "[2001:db8::113:1]:8080",
+			},
+			expectIP: "2001:db8::113:199",
+		},
 		{
 			name: "request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
 			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
@@ -520,6 +544,20 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.199",
 		},
+		{
+			name: "request is from external IP has XFF and valid + TRUSTED X-Real-Ip header, extract IP from X-Real-Ip header",
+			givenTrustOptions: []TrustOption{ // case for "trust direct-facing proxy"
+				TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64"
+			},
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXRealIP:       []string{"[2001:db8::113:199]"},
+					HeaderXForwardedFor: []string{"[2001:db8::113:198], [2001:db8::113:197]"}, // <-- should not affect anything
+				},
+				RemoteAddr: "[2001:db8::113:1]:8080",
+			},
+			expectIP: "2001:db8::113:199",
+		},
 	}
 
 	for _, tc := range testCases {
@@ -532,6 +570,7 @@ func TestExtractIPFromRealIPHeader(t *testing.T) {
 
 func TestExtractIPFromXFFHeader(t *testing.T) {
 	_, ipForRemoteAddrExternalRange, _ := net.ParseCIDR("203.0.113.199/24")
+	_, ipv6ForRemoteAddrExternalRange, _ := net.ParseCIDR("2001:db8::/64")
 
 	var testCases = []struct {
 		name              string
@@ -566,6 +605,16 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
 			},
 			expectIP: "127.0.0.3",
 		},
+		{
+			name: "request trusts all IPs in XFF header, extract IP from furthest in XFF chain",
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXForwardedFor: []string{"[fe80::3], [fe80::2], [fe80::1]"},
+				},
+				RemoteAddr: "[fe80::1]:8080",
+			},
+			expectIP: "fe80::3",
+		},
 		{
 			name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr",
 			whenRequest: http.Request{
@@ -576,6 +625,16 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
 			},
 			expectIP: "203.0.113.1",
 		},
+		{
+			name: "request is from external IP has valid + UNTRUSTED external XFF header, extract IP from remote addr",
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXForwardedFor: []string{"[2001:db8::1]"}, // <-- this is untrusted
+				},
+				RemoteAddr: "[2001:db8::2]:8080",
+			},
+			expectIP: "2001:db8::2",
+		},
 		{
 			name: "request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header",
 			givenTrustOptions: []TrustOption{
@@ -595,6 +654,25 @@ func TestExtractIPFromXFFHeader(t *testing.T) {
 			},
 			expectIP: "203.0.100.100", // this is first trusted IP in XFF chain
 		},
+		{
+			name: "request is from external IP is valid and has some IPs TRUSTED XFF header, extract IP from XFF header",
+			givenTrustOptions: []TrustOption{
+				TrustIPRange(ipv6ForRemoteAddrExternalRange), // we trust external IP range "2001:db8::/64"
+			},
+			// from request its seems that request has been proxied through 6 servers.
+			// 1) 2001:db8:1::1:100 (this is external IP set by 2001:db8:2::100:100 which we do not trust - could be spoofed)
+			// 2) 2001:db8:2::100:100  (this is outside of our network but set by 2001:db8::113:199 which we trust to set correct IPs)
+			// 3) 2001:db8::113:199 (we trust, for example maybe our proxy from some other office)
+			// 4) fd12:3456:789a:1::1 (internal IP, some internal upstream loadbalancer ala SSL offloading with F5 products)
+			// 5) fe80::1 (is proxy on localhost. maybe we have Nginx in front of our Echo instance doing some routing)
+			whenRequest: http.Request{
+				Header: http.Header{
+					HeaderXForwardedFor: []string{"[2001:db8:1::1:100], [2001:db8:2::100:100], [2001:db8::113:199], [fd12:3456:789a:1::1]"},
+				},
+				RemoteAddr: "[fe80::1]:8080", // IP of proxy upstream of our APP
+			},
+			expectIP: "2001:db8:2::100:100", // this is first trusted IP in XFF chain
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/middleware/body_dump_test.go b/middleware/body_dump_test.go
index 533971a..de1de33 100644
--- a/middleware/body_dump_test.go
+++ b/middleware/body_dump_test.go
@@ -2,7 +2,7 @@ package middleware
 
 import (
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -19,7 +19,7 @@ func TestBodyDump(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	h := func(c echo.Context) error {
-		body, err := ioutil.ReadAll(c.Request().Body)
+		body, err := io.ReadAll(c.Request().Body)
 		if err != nil {
 			return err
 		}
diff --git a/middleware/body_limit_test.go b/middleware/body_limit_test.go
index b891767..2bfce37 100644
--- a/middleware/body_limit_test.go
+++ b/middleware/body_limit_test.go
@@ -2,7 +2,7 @@ package middleware
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -18,7 +18,7 @@ func TestBodyLimit(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	h := func(c echo.Context) error {
-		body, err := ioutil.ReadAll(c.Request().Body)
+		body, err := io.ReadAll(c.Request().Body)
 		if err != nil {
 			return err
 		}
@@ -67,19 +67,108 @@ func TestBodyLimitReader(t *testing.T) {
 	}
 	reader := &limitedReader{
 		BodyLimitConfig: config,
-		reader:          ioutil.NopCloser(bytes.NewReader(hw)),
+		reader:          io.NopCloser(bytes.NewReader(hw)),
 		context:         e.NewContext(req, rec),
 	}
 
 	// read all should return ErrStatusRequestEntityTooLarge
-	_, err := ioutil.ReadAll(reader)
+	_, err := io.ReadAll(reader)
 	he := err.(*echo.HTTPError)
 	assert.Equal(t, http.StatusRequestEntityTooLarge, he.Code)
 
 	// reset reader and read two bytes must succeed
 	bt := make([]byte, 2)
-	reader.Reset(ioutil.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec))
+	reader.Reset(io.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec))
 	n, err := reader.Read(bt)
 	assert.Equal(t, 2, n)
 	assert.Equal(t, nil, err)
 }
+
+func TestBodyLimitWithConfig_Skipper(t *testing.T) {
+	e := echo.New()
+	h := func(c echo.Context) error {
+		body, err := io.ReadAll(c.Request().Body)
+		if err != nil {
+			return err
+		}
+		return c.String(http.StatusOK, string(body))
+	}
+	mw := BodyLimitWithConfig(BodyLimitConfig{
+		Skipper: func(c echo.Context) bool {
+			return true
+		},
+		Limit: "2B", // if not skipped this limit would make request to fail limit check
+	})
+
+	hw := []byte("Hello, World!")
+	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw))
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	err := mw(h)(c)
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, rec.Code)
+	assert.Equal(t, hw, rec.Body.Bytes())
+}
+
+func TestBodyLimitWithConfig(t *testing.T) {
+	var testCases = []struct {
+		name        string
+		givenLimit  string
+		whenBody    []byte
+		expectBody  []byte
+		expectError string
+	}{
+		{
+			name:        "ok, body is less than limit",
+			givenLimit:  "10B",
+			whenBody:    []byte("123456789"),
+			expectBody:  []byte("123456789"),
+			expectError: "",
+		},
+		{
+			name:        "nok, body is more than limit",
+			givenLimit:  "9B",
+			whenBody:    []byte("1234567890"),
+			expectBody:  []byte(nil),
+			expectError: "code=413, message=Request Entity Too Large",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
+			h := func(c echo.Context) error {
+				body, err := io.ReadAll(c.Request().Body)
+				if err != nil {
+					return err
+				}
+				return c.String(http.StatusOK, string(body))
+			}
+			mw := BodyLimitWithConfig(BodyLimitConfig{
+				Limit: tc.givenLimit,
+			})
+
+			req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(tc.whenBody))
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
+
+			err := mw(h)(c)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+			// not testing status as middlewares return error instead of committing it and OK cases are anyway 200
+			assert.Equal(t, tc.expectBody, rec.Body.Bytes())
+		})
+	}
+}
+
+func TestBodyLimit_panicOnInvalidLimit(t *testing.T) {
+	assert.PanicsWithError(
+		t,
+		"echo: invalid body-limit=",
+		func() { BodyLimit("") },
+	)
+}
diff --git a/middleware/compress_test.go b/middleware/compress_test.go
index c8dd2e1..714548e 100644
--- a/middleware/compress_test.go
+++ b/middleware/compress_test.go
@@ -4,9 +4,9 @@ import (
 	"bytes"
 	"compress/gzip"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"testing"
 
 	"github.com/labstack/echo/v4"
@@ -173,7 +173,7 @@ func TestGzipWithStatic(t *testing.T) {
 	r, err := gzip.NewReader(rec.Body)
 	if assert.NoError(t, err) {
 		defer r.Close()
-		want, err := ioutil.ReadFile("../_fixture/images/walle.png")
+		want, err := os.ReadFile("../_fixture/images/walle.png")
 		if assert.NoError(t, err) {
 			buf := new(bytes.Buffer)
 			buf.ReadFrom(r)
diff --git a/middleware/decompress_test.go b/middleware/decompress_test.go
index 42c6250..2e73ba8 100644
--- a/middleware/decompress_test.go
+++ b/middleware/decompress_test.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -39,7 +39,7 @@ func TestDecompress(t *testing.T) {
 	c = e.NewContext(req, rec)
 	h(c)
 	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
+	b, err := io.ReadAll(req.Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(b))
 }
@@ -67,7 +67,7 @@ func TestDecompressDefaultConfig(t *testing.T) {
 	c = e.NewContext(req, rec)
 	h(c)
 	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
+	b, err := io.ReadAll(req.Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(b))
 }
@@ -82,7 +82,7 @@ func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) {
 	e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
+	b, err := io.ReadAll(req.Body)
 	assert.NoError(t, err)
 	assert.NotEqual(t, b, body)
 	assert.Equal(t, b, gz)
@@ -132,7 +132,7 @@ func TestDecompressSkipper(t *testing.T) {
 	c := e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8)
-	reqBody, err := ioutil.ReadAll(c.Request().Body)
+	reqBody, err := io.ReadAll(c.Request().Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(reqBody))
 }
@@ -161,7 +161,7 @@ func TestDecompressPoolError(t *testing.T) {
 	c := e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	reqBody, err := ioutil.ReadAll(c.Request().Body)
+	reqBody, err := io.ReadAll(c.Request().Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(reqBody))
 	assert.Equal(t, rec.Code, http.StatusInternalServerError)
diff --git a/middleware/logger_test.go b/middleware/logger_test.go
index ab889bf..a056861 100644
--- a/middleware/logger_test.go
+++ b/middleware/logger_test.go
@@ -173,6 +173,28 @@ func TestLoggerCustomTimestamp(t *testing.T) {
 	assert.Error(t, err)
 }
 
+func TestLoggerCustomTagFunc(t *testing.T) {
+	e := echo.New()
+	buf := new(bytes.Buffer)
+	e.Use(LoggerWithConfig(LoggerConfig{
+		Format: `{"method":"${method}",${custom}}` + "\n",
+		CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) {
+			return buf.WriteString(`"tag":"my-value"`)
+		},
+		Output: buf,
+	}))
+
+	e.GET("/", func(c echo.Context) error {
+		return c.String(http.StatusOK, "custom time stamp test")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, `{"method":"GET","tag":"my-value"}`+"\n", buf.String())
+}
+
 func BenchmarkLoggerWithConfig_withoutMapFields(b *testing.B) {
 	e := echo.New()
 
diff --git a/middleware/proxy_test.go b/middleware/proxy_test.go
index 0ded50a..a1b7f2c 100644
--- a/middleware/proxy_test.go
+++ b/middleware/proxy_test.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -93,7 +93,7 @@ func TestProxy(t *testing.T) {
 	e.Use(ProxyWithConfig(ProxyConfig{
 		Balancer: rrb,
 		ModifyResponse: func(res *http.Response) error {
-			res.Body = ioutil.NopCloser(bytes.NewBuffer([]byte("modified")))
+			res.Body = io.NopCloser(bytes.NewBuffer([]byte("modified")))
 			res.Header.Set("X-Modified", "1")
 			return nil
 		},
@@ -384,10 +384,9 @@ func TestProxyError(t *testing.T) {
 	e := echo.New()
 	e.Use(Proxy(rb))
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	rec := httptest.NewRecorder()
 
 	// Remote unreachable
-	rec = httptest.NewRecorder()
+	rec := httptest.NewRecorder()
 	req.URL.Path = "/api/users"
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, "/api/users", req.URL.Path)
diff --git a/middleware/request_logger_test.go b/middleware/request_logger_test.go
index 5118b12..51d617a 100644
--- a/middleware/request_logger_test.go
+++ b/middleware/request_logger_test.go
@@ -103,12 +103,12 @@ func TestRequestLogger_beforeNextFunc(t *testing.T) {
 func TestRequestLogger_logError(t *testing.T) {
 	e := echo.New()
 
-	var expect RequestLoggerValues
+	var actual RequestLoggerValues
 	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
 		LogError:  true,
 		LogStatus: true,
 		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
-			expect = values
+			actual = values
 			return nil
 		},
 	}))
@@ -123,8 +123,52 @@ func TestRequestLogger_logError(t *testing.T) {
 	e.ServeHTTP(rec, req)
 
 	assert.Equal(t, http.StatusNotAcceptable, rec.Code)
-	assert.Equal(t, http.StatusNotAcceptable, expect.Status)
-	assert.EqualError(t, expect.Error, "code=406, message=nope")
+	assert.Equal(t, http.StatusNotAcceptable, actual.Status)
+	assert.EqualError(t, actual.Error, "code=406, message=nope")
+}
+
+func TestRequestLogger_HandleError(t *testing.T) {
+	e := echo.New()
+
+	var actual RequestLoggerValues
+	e.Use(RequestLoggerWithConfig(RequestLoggerConfig{
+		timeNow: func() time.Time {
+			return time.Unix(1631045377, 0).UTC()
+		},
+		HandleError: true,
+		LogError:    true,
+		LogStatus:   true,
+		LogValuesFunc: func(c echo.Context, values RequestLoggerValues) error {
+			actual = values
+			return nil
+		},
+	}))
+
+	// to see if "HandleError" works we create custom error handler that uses its own status codes
+	e.HTTPErrorHandler = func(err error, c echo.Context) {
+		if c.Response().Committed {
+			return
+		}
+		c.JSON(http.StatusTeapot, "custom error handler")
+	}
+
+	e.GET("/test", func(c echo.Context) error {
+		return echo.NewHTTPError(http.StatusForbidden, "nope")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/test", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusTeapot, rec.Code)
+
+	expect := RequestLoggerValues{
+		StartTime: time.Unix(1631045377, 0).UTC(),
+		Status:    http.StatusTeapot,
+		Error:     echo.NewHTTPError(http.StatusForbidden, "nope"),
+	}
+	assert.Equal(t, expect, actual)
 }
 
 func TestRequestLogger_LogValuesFuncError(t *testing.T) {
diff --git a/middleware/rewrite_test.go b/middleware/rewrite_test.go
index 0ac04bb..47d707c 100644
--- a/middleware/rewrite_test.go
+++ b/middleware/rewrite_test.go
@@ -1,7 +1,7 @@
 package middleware
 
 import (
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -142,7 +142,7 @@ func TestRewriteWithConfigPreMiddleware_Issue1143(t *testing.T) {
 		assert.Equal(t, http.StatusOK, rec.Code)
 
 		defer rec.Result().Body.Close()
-		bodyBytes, _ := ioutil.ReadAll(rec.Result().Body)
+		bodyBytes, _ := io.ReadAll(rec.Result().Body)
 		assert.Equal(t, "hosts", string(bodyBytes))
 	}
 }
diff --git a/middleware/timeout_test.go b/middleware/timeout_test.go
index 56eb7bc..6f60753 100644
--- a/middleware/timeout_test.go
+++ b/middleware/timeout_test.go
@@ -5,7 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"log"
 	"net"
 	"net/http"
@@ -129,7 +129,7 @@ func TestTimeoutOnTimeoutRouteErrorHandler(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
-	stopChan := make(chan struct{}, 0)
+	stopChan := make(chan struct{})
 	err := m(func(c echo.Context) error {
 		<-stopChan
 		return errors.New("error in route after timeout")
@@ -245,7 +245,7 @@ func TestTimeoutWithErrorMessage(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
-	stopChan := make(chan struct{}, 0)
+	stopChan := make(chan struct{})
 	err := m(func(c echo.Context) error {
 		// NOTE: when difference between timeout duration and handler execution time is almost the same (in range of 100microseconds)
 		// the result of timeout does not seem to be reliable - could respond timeout, could respond handler output
@@ -275,7 +275,7 @@ func TestTimeoutWithDefaultErrorMessage(t *testing.T) {
 	e := echo.New()
 	c := e.NewContext(req, rec)
 
-	stopChan := make(chan struct{}, 0)
+	stopChan := make(chan struct{})
 	err := m(func(c echo.Context) error {
 		<-stopChan
 		return c.String(http.StatusOK, "Hello, World!")
@@ -410,7 +410,7 @@ func TestTimeoutWithFullEchoStack(t *testing.T) {
 			}
 
 			assert.Equal(t, tc.expectStatusCode, res.StatusCode)
-			if body, err := ioutil.ReadAll(res.Body); err == nil {
+			if body, err := io.ReadAll(res.Body); err == nil {
 				assert.Equal(t, tc.expectResponse, string(body))
 			} else {
 				assert.Fail(t, err.Error())