Repository: labstack/echo
Pull request #2461 (2023-06-13 16:38:30)

Add Skipper Unit Test In BasicBasicAuthConfig and Add More Detail Explanation regarding BasicAuthValidator
I Think Better To Add Skipper Sample Instead Of Nil, And The Explanation of BasicAuthValidator i think can me make more clear

RyoKusnadi: Hi @aldas, mind to review this PR? Thanks!

Patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index d2d3386..9ae5dbd 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -14,17 +14,17 @@ permissions:
 
 env:
   # run static analysis only with the latest Go version
-  LATEST_GO_VERSION: "1.20"
+  LATEST_GO_VERSION: "1.22"
 
 jobs:
   check:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout Code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Set up Go ${{ matrix.go }}
-        uses: actions/setup-go@v3
+        uses: actions/setup-go@v5
         with:
           go-version: ${{ env.LATEST_GO_VERSION }}
           check-latest: true
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index e06183d..cb3dc44 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -14,7 +14,7 @@ permissions:
 
 env:
   # run coverage and benchmarks only with the latest Go version
-  LATEST_GO_VERSION: "1.20"
+  LATEST_GO_VERSION: "1.22"
 
 jobs:
   test:
@@ -25,15 +25,15 @@ jobs:
         # Echo tests with last four major releases (unless there are pressing vulnerabilities)
         # As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when
         # we derive from last four major releases promise.
-        go: ["1.18", "1.19", "1.20"]
+        go: ["1.19", "1.20", "1.21", "1.22"]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
       - name: Checkout Code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Set up Go ${{ matrix.go }}
-        uses: actions/setup-go@v3
+        uses: actions/setup-go@v5
         with:
           go-version: ${{ matrix.go }}
 
@@ -53,18 +53,18 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout Code (Previous)
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ github.base_ref }}
           path: previous
 
       - name: Checkout Code (New)
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           path: new
 
       - name: Set up Go ${{ matrix.go }}
-        uses: actions/setup-go@v3
+        uses: actions/setup-go@v5
         with:
           go-version: ${{ env.LATEST_GO_VERSION }}
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8318424..cc17e28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,79 @@
 # Changelog
 
+## v4.11.4 - 2023-12-20
+
+**Security**
+
+* Upgrade golang.org/x/crypto to v0.17.0 to fix vulnerability [issue](https://pkg.go.dev/vuln/GO-2023-2402) [#2562](https://github.com/labstack/echo/pull/2562)
+
+**Enhancements**
+
+* Update deps and mark Go version to 1.18 as this is what golang.org/x/* use [#2563](https://github.com/labstack/echo/pull/2563)
+* Request logger: add example for Slog https://pkg.go.dev/log/slog [#2543](https://github.com/labstack/echo/pull/2543)
+
+
+## v4.11.3 - 2023-11-07
+
+**Security**
+
+* 'c.Attachment' and 'c.Inline' should escape filename in 'Content-Disposition' header to avoid 'Reflect File Download' vulnerability. [#2541](https://github.com/labstack/echo/pull/2541)
+
+**Enhancements**
+
+* Tests: refactor context tests to be separate functions [#2540](https://github.com/labstack/echo/pull/2540)
+* Proxy middleware: reuse echo request context [#2537](https://github.com/labstack/echo/pull/2537)
+* Mark unmarshallable yaml struct tags as ignored [#2536](https://github.com/labstack/echo/pull/2536)
+
+
+## v4.11.2 - 2023-10-11
+
+**Security**
+
+* Bump golang.org/x/net to prevent CVE-2023-39325 / CVE-2023-44487 HTTP/2 Rapid Reset Attack [#2527](https://github.com/labstack/echo/pull/2527)
+* fix(sec): randomString bias introduced by #2490 [#2492](https://github.com/labstack/echo/pull/2492)
+* CSRF/RequestID mw: switch math/random usage to crypto/random [#2490](https://github.com/labstack/echo/pull/2490)
+
+**Enhancements**
+
+* Delete unused context in body_limit.go [#2483](https://github.com/labstack/echo/pull/2483)
+* Use Go 1.21 in CI [#2505](https://github.com/labstack/echo/pull/2505)
+* Fix some typos [#2511](https://github.com/labstack/echo/pull/2511)
+* Allow CORS middleware to send Access-Control-Max-Age: 0 [#2518](https://github.com/labstack/echo/pull/2518)
+* Bump dependancies [#2522](https://github.com/labstack/echo/pull/2522)
+
+## v4.11.1 - 2023-07-16
+
+**Fixes**
+
+* Fix `Gzip` middleware not sending response code for no content responses (404, 301/302 redirects etc) [#2481](https://github.com/labstack/echo/pull/2481)
+
+
+## v4.11.0 - 2023-07-14
+
+
+**Fixes**
+
+* Fixes the proxy middleware concurrency issue of calling the Next() proxy target on Round Robin Balancer [#2409](https://github.com/labstack/echo/pull/2409)
+* Fix `group.RouteNotFound` not working when group has attached middlewares [#2411](https://github.com/labstack/echo/pull/2411)
+* Fix global error handler return error message when message is an error [#2456](https://github.com/labstack/echo/pull/2456)
+* Do not use global timeNow variables [#2477](https://github.com/labstack/echo/pull/2477)
+
+
+**Enhancements**
+
+* Added a optional config variable to disable centralized error handler in recovery middleware [#2410](https://github.com/labstack/echo/pull/2410)
+* refactor: use `strings.ReplaceAll` directly [#2424](https://github.com/labstack/echo/pull/2424)
+* Add support for Go1.20 `http.rwUnwrapper` to Response struct [#2425](https://github.com/labstack/echo/pull/2425)
+* Check whether is nil before invoking centralized error handling [#2429](https://github.com/labstack/echo/pull/2429)
+* Proper colon support in `echo.Reverse` method [#2416](https://github.com/labstack/echo/pull/2416)
+* Fix misuses of a vs an in documentation comments [#2436](https://github.com/labstack/echo/pull/2436)
+* Add link to slog.Handler library for Echo logging into README.md [#2444](https://github.com/labstack/echo/pull/2444)
+* In proxy middleware Support retries of failed proxy requests [#2414](https://github.com/labstack/echo/pull/2414)
+* gofmt fixes to comments [#2452](https://github.com/labstack/echo/pull/2452)
+* gzip response only if it exceeds a minimal length [#2267](https://github.com/labstack/echo/pull/2267)
+* Upgrade packages [#2475](https://github.com/labstack/echo/pull/2475)
+
+
 ## v4.10.2 - 2023-02-22
 
 **Security**
diff --git a/README.md b/README.md
index ea8f30f..351ba3c 100644
--- a/README.md
+++ b/README.md
@@ -3,26 +3,24 @@
 [![Sourcegraph](https://sourcegraph.com/github.com/labstack/echo/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/labstack/echo?badge)
 [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/labstack/echo/v4)
 [![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)
+[![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/labstack/echo/echo.yml?style=flat-square)](https://github.com/labstack/echo/actions)
 [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/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)
 
-## Supported Go versions
+## Echo
 
-Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with
-older versions.
+High performance, extensible, minimalist Go web framework.
 
-As of version 4.0.0, Echo is available as a [Go module](https://github.com/golang/go/wiki/Modules).
-Therefore a Go version capable of understanding /vN suffixed imports is required:
+* [Official website](https://echo.labstack.com)
+* [Quick start](https://echo.labstack.com/docs/quick-start)
+* [Middlewares](https://echo.labstack.com/docs/category/middleware)
 
-Any of these versions will allow you to import Echo as `github.com/labstack/echo/v4` which is the recommended
-way of using Echo going forward.
+Help and questions: [Github Discussions](https://github.com/labstack/echo/discussions)
 
-For older versions, please use the latest v3 tag.
 
-## Feature Overview
+### Feature Overview
 
 - Optimized HTTP router which smartly prioritize routes
 - Build robust and scalable RESTful APIs
@@ -38,6 +36,18 @@ For older versions, please use the latest v3 tag.
 - Automatic TLS via Let’s Encrypt
 - HTTP/2 support
 
+## Sponsors
+
+<div>
+  <a href="https://encore.dev" style="display: inline-flex; align-items: center; gap: 10px">
+    <img src="https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg" height="28px" alt="encore icon"></img>
+  <b>Encore – the platform for building Go-based cloud backends</b>
+    </a>
+</div>
+<br/>
+
+Click [here](https://github.com/sponsors/labstack) for more information on sponsorship.
+
 ## Benchmarks
 
 Date: 2020/11/11<br>
@@ -57,6 +67,7 @@ The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
 // go get github.com/labstack/echo/{version}
 go get github.com/labstack/echo/v4
 ```
+Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with older versions.
 
 ### Example
 
@@ -117,10 +128,6 @@ of middlewares in this list.
 
 Please send a PR to add your own library here.
 
-## Help
-
-- [Forum](https://github.com/labstack/echo/discussions)
-
 ## Contribute
 
 **Use issues for everything**
diff --git a/bind.go b/bind.go
index 374a2ae..51f4689 100644
--- a/bind.go
+++ b/bind.go
@@ -131,10 +131,29 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
 	typ := reflect.TypeOf(destination).Elem()
 	val := reflect.ValueOf(destination).Elem()
 
-	// Map
-	if typ.Kind() == reflect.Map {
+	// Support binding to limited Map destinations:
+	// - map[string][]string,
+	// - map[string]string <-- (binds first value from data slice)
+	// - map[string]interface{}
+	// You are better off binding to struct but there are user who want this map feature. Source of data for these cases are:
+	// params,query,header,form as these sources produce string values, most of the time slice of strings, actually.
+	if typ.Kind() == reflect.Map && typ.Key().Kind() == reflect.String {
+		k := typ.Elem().Kind()
+		isElemInterface := k == reflect.Interface
+		isElemString := k == reflect.String
+		isElemSliceOfStrings := k == reflect.Slice && typ.Elem().Elem().Kind() == reflect.String
+		if !(isElemSliceOfStrings || isElemString || isElemInterface) {
+			return nil
+		}
+		if val.IsNil() {
+			val.Set(reflect.MakeMap(typ))
+		}
 		for k, v := range data {
-			val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))
+			if isElemString {
+				val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))
+			} else {
+				val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v))
+			}
 		}
 		return nil
 	}
diff --git a/binder.go b/binder.go
index 29cceca..8e7b814 100644
--- a/binder.go
+++ b/binder.go
@@ -1323,7 +1323,7 @@ func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExi
 	case time.Second:
 		*dest = time.Unix(n, 0)
 	case time.Millisecond:
-		*dest = time.Unix(n/1e3, (n%1e3)*1e6) // TODO: time.UnixMilli(n) exists since Go1.17 switch to that when min version allows
+		*dest = time.UnixMilli(n)
 	case time.Nanosecond:
 		*dest = time.Unix(0, n)
 	}
diff --git a/context.go b/context.go
index 27da28a..d4cba84 100644
--- a/context.go
+++ b/context.go
@@ -489,7 +489,7 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
 }
 
 func (c *context) json(code int, i interface{}, indent string) error {
-	c.writeContentType(MIMEApplicationJSONCharsetUTF8)
+	c.writeContentType(MIMEApplicationJSON)
 	c.response.Status = code
 	return c.echo.JSONSerializer.Serialize(c, i, indent)
 }
@@ -507,7 +507,7 @@ func (c *context) JSONPretty(code int, i interface{}, indent string) (err error)
 }
 
 func (c *context) JSONBlob(code int, b []byte) (err error) {
-	return c.Blob(code, MIMEApplicationJSONCharsetUTF8, b)
+	return c.Blob(code, MIMEApplicationJSON, b)
 }
 
 func (c *context) JSONP(code int, callback string, i interface{}) (err error) {
@@ -584,8 +584,10 @@ func (c *context) Inline(file, name string) error {
 	return c.contentDisposition(file, name, "inline")
 }
 
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
 func (c *context) contentDisposition(file, name, dispositionType string) error {
-	c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name))
+	c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, dispositionType, quoteEscaper.Replace(name)))
 	return c.File(file)
 }
 
diff --git a/echo.go b/echo.go
index e216354..7b6a090 100644
--- a/echo.go
+++ b/echo.go
@@ -169,7 +169,12 @@ const (
 
 // MIME types
 const (
-	MIMEApplicationJSON                  = "application/json"
+	// MIMEApplicationJSON JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259
+	MIMEApplicationJSON = "application/json"
+	// Deprecated: Please use MIMEApplicationJSON instead. JSON should be encoded using UTF-8 by default.
+	// No "charset" parameter is defined for this registration.
+	// Adding one really has no effect on compliant recipients.
+	// See RFC 8259, section 8.1. https://datatracker.ietf.org/doc/html/rfc8259#section-8.1
 	MIMEApplicationJSONCharsetUTF8       = MIMEApplicationJSON + "; " + charsetUTF8
 	MIMEApplicationJavaScript            = "application/javascript"
 	MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
@@ -259,7 +264,7 @@ const (
 
 const (
 	// Version of Echo
-	Version = "4.10.2"
+	Version = "4.11.4"
 	website = "https://echo.labstack.com"
 	// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
 	banner = `
diff --git a/go.mod b/go.mod
index 265b0aa..089ffb1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,24 +1,24 @@
 module github.com/labstack/echo/v4
 
-go 1.17
+go 1.18
 
 require (
 	github.com/golang-jwt/jwt v3.2.2+incompatible
-	github.com/labstack/gommon v0.4.0
-	github.com/stretchr/testify v1.8.1
+	github.com/labstack/gommon v0.4.2
+	github.com/stretchr/testify v1.8.4
 	github.com/valyala/fasttemplate v1.2.2
-	golang.org/x/crypto v0.6.0
-	golang.org/x/net v0.7.0
-	golang.org/x/time v0.3.0
+	golang.org/x/crypto v0.17.0
+	golang.org/x/net v0.19.0
+	golang.org/x/time v0.5.0
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
-	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	golang.org/x/text v0.7.0 // indirect
+	golang.org/x/sys v0.15.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 79ff318..0584b7e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,74 +1,35 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
-github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
-github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/middleware/basic_auth.go b/middleware/basic_auth.go
index f9e8caa..07a5761 100644
--- a/middleware/basic_auth.go
+++ b/middleware/basic_auth.go
@@ -25,6 +25,8 @@ type (
 	}
 
 	// BasicAuthValidator defines a function to validate BasicAuth credentials.
+	// The function should return a boolean indicating whether the credentials are valid,
+	// and an error if any error occurs during the validation process.
 	BasicAuthValidator func(string, string, echo.Context) (bool, error)
 )
 
diff --git a/middleware/body_limit.go b/middleware/body_limit.go
index b436bd5..99e3ac5 100644
--- a/middleware/body_limit.go
+++ b/middleware/body_limit.go
@@ -23,9 +23,8 @@ type (
 
 	limitedReader struct {
 		BodyLimitConfig
-		reader  io.ReadCloser
-		read    int64
-		context echo.Context
+		reader io.ReadCloser
+		read   int64
 	}
 )
 
@@ -80,7 +79,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
 
 			// Based on content read
 			r := pool.Get().(*limitedReader)
-			r.Reset(req.Body, c)
+			r.Reset(req.Body)
 			defer pool.Put(r)
 			req.Body = r
 
@@ -102,9 +101,8 @@ func (r *limitedReader) Close() error {
 	return r.reader.Close()
 }
 
-func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) {
+func (r *limitedReader) Reset(reader io.ReadCloser) {
 	r.reader = reader
-	r.context = context
 	r.read = 0
 }
 
diff --git a/middleware/compress.go b/middleware/compress.go
index cbe29fc..3e9bd32 100644
--- a/middleware/compress.go
+++ b/middleware/compress.go
@@ -107,10 +107,16 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 
 				grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
 				defer func() {
+					// There are different reasons for cases when we have not yet written response to the client and now need to do so.
+					// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
+					// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
 					if !grw.wroteBody {
 						if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {
 							res.Header().Del(echo.HeaderContentEncoding)
 						}
+						if grw.wroteHeader {
+							rw.WriteHeader(grw.code)
+						}
 						// We have to reset response to it's pristine state when
 						// nothing is written to body or error is returned.
 						// See issue #424, #407.
diff --git a/middleware/context_timeout.go b/middleware/context_timeout.go
index be260e1..1937693 100644
--- a/middleware/context_timeout.go
+++ b/middleware/context_timeout.go
@@ -13,7 +13,7 @@ type ContextTimeoutConfig struct {
 	// Skipper defines a function to skip middleware.
 	Skipper Skipper
 
-	// ErrorHandler is a function when error aries in middeware execution.
+	// ErrorHandler is a function when error aries in middleware execution.
 	ErrorHandler func(err error, c echo.Context) error
 
 	// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
diff --git a/middleware/cors.go b/middleware/cors.go
index 6ddb540..7ace2f2 100644
--- a/middleware/cors.go
+++ b/middleware/cors.go
@@ -39,7 +39,7 @@ type (
 		// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
 		//
 		// Optional.
-		AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"`
+		AllowOriginFunc func(origin string) (bool, error) `yaml:"-"`
 
 		// AllowMethods determines the value of the Access-Control-Allow-Methods
 		// response header.  This header specified the list of methods allowed when
@@ -99,8 +99,9 @@ type (
 		// MaxAge determines the value of the Access-Control-Max-Age response header.
 		// This header indicates how long (in seconds) the results of a preflight
 		// request can be cached.
+		// The header is set only if MaxAge != 0, negative value sends "0" which instructs browsers not to cache that response.
 		//
-		// Optional. Default value 0.  The header is set only if MaxAge > 0.
+		// Optional. Default value 0 - meaning header is not sent.
 		//
 		// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
 		MaxAge int `yaml:"max_age"`
@@ -159,7 +160,11 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 	allowMethods := strings.Join(config.AllowMethods, ",")
 	allowHeaders := strings.Join(config.AllowHeaders, ",")
 	exposeHeaders := strings.Join(config.ExposeHeaders, ",")
-	maxAge := strconv.Itoa(config.MaxAge)
+
+	maxAge := "0"
+	if config.MaxAge > 0 {
+		maxAge = strconv.Itoa(config.MaxAge)
+	}
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
 		return func(c echo.Context) error {
@@ -282,7 +287,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 					res.Header().Set(echo.HeaderAccessControlAllowHeaders, h)
 				}
 			}
-			if config.MaxAge > 0 {
+			if config.MaxAge != 0 {
 				res.Header().Set(echo.HeaderAccessControlMaxAge, maxAge)
 			}
 			return c.NoContent(http.StatusNoContent)
diff --git a/middleware/csrf.go b/middleware/csrf.go
index 6899700..adf1221 100644
--- a/middleware/csrf.go
+++ b/middleware/csrf.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/labstack/echo/v4"
-	"github.com/labstack/gommon/random"
 )
 
 type (
@@ -103,6 +102,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 	if config.TokenLength == 0 {
 		config.TokenLength = DefaultCSRFConfig.TokenLength
 	}
+
 	if config.TokenLookup == "" {
 		config.TokenLookup = DefaultCSRFConfig.TokenLookup
 	}
@@ -132,7 +132,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 
 			token := ""
 			if k, err := c.Cookie(config.CookieName); err != nil {
-				token = random.String(config.TokenLength) // Generate token
+				token = randomString(config.TokenLength)
 			} else {
 				token = k.Value // Reuse token
 			}
diff --git a/middleware/proxy.go b/middleware/proxy.go
index e4f98d9..16b00d6 100644
--- a/middleware/proxy.go
+++ b/middleware/proxy.go
@@ -359,6 +359,10 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
 					c.Set("_error", nil)
 				}
 
+				// This is needed for ProxyConfig.ModifyResponse and/or ProxyConfig.Transport to be able to process the Request
+				// that Balancer may have replaced with c.SetRequest.
+				req = c.Request()
+
 				// Proxy
 				switch {
 				case c.IsWebSocket():
diff --git a/middleware/rate_limiter.go b/middleware/rate_limiter.go
index f7fae83..1d24df5 100644
--- a/middleware/rate_limiter.go
+++ b/middleware/rate_limiter.go
@@ -160,6 +160,8 @@ type (
 		burst       int
 		expiresIn   time.Duration
 		lastCleanup time.Time
+
+		timeNow func() time.Time
 	}
 	// Visitor signifies a unique user's limiter details
 	Visitor struct {
@@ -219,7 +221,8 @@ func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (s
 		store.burst = int(config.Rate)
 	}
 	store.visitors = make(map[string]*Visitor)
-	store.lastCleanup = now()
+	store.timeNow = time.Now
+	store.lastCleanup = store.timeNow()
 	return
 }
 
@@ -244,12 +247,13 @@ func (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) {
 		limiter.Limiter = rate.NewLimiter(store.rate, store.burst)
 		store.visitors[identifier] = limiter
 	}
-	limiter.lastSeen = now()
-	if now().Sub(store.lastCleanup) > store.expiresIn {
+	now := store.timeNow()
+	limiter.lastSeen = now
+	if now.Sub(store.lastCleanup) > store.expiresIn {
 		store.cleanupStaleVisitors()
 	}
 	store.mutex.Unlock()
-	return limiter.AllowN(now(), 1), nil
+	return limiter.AllowN(store.timeNow(), 1), nil
 }
 
 /*
@@ -258,14 +262,9 @@ of users who haven't visited again after the configured expiry time has elapsed
 */
 func (store *RateLimiterMemoryStore) cleanupStaleVisitors() {
 	for id, visitor := range store.visitors {
-		if now().Sub(visitor.lastSeen) > store.expiresIn {
+		if store.timeNow().Sub(visitor.lastSeen) > store.expiresIn {
 			delete(store.visitors, id)
 		}
 	}
-	store.lastCleanup = now()
+	store.lastCleanup = store.timeNow()
 }
-
-/*
-actual time method which is mocked in test file
-*/
-var now = time.Now
diff --git a/middleware/request_id.go b/middleware/request_id.go
index 8c5ff66..e29c8f5 100644
--- a/middleware/request_id.go
+++ b/middleware/request_id.go
@@ -2,7 +2,6 @@ package middleware
 
 import (
 	"github.com/labstack/echo/v4"
-	"github.com/labstack/gommon/random"
 )
 
 type (
@@ -12,7 +11,7 @@ type (
 		Skipper Skipper
 
 		// Generator defines a function to generate an ID.
-		// Optional. Default value random.String(32).
+		// Optional. Defaults to generator for random string of length 32.
 		Generator func() string
 
 		// RequestIDHandler defines a function which is executed for a request id.
@@ -73,5 +72,5 @@ func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
 }
 
 func generator() string {
-	return random.String(32)
+	return randomString(32)
 }
diff --git a/middleware/request_logger.go b/middleware/request_logger.go
index 8e312e8..f82f6b6 100644
--- a/middleware/request_logger.go
+++ b/middleware/request_logger.go
@@ -8,6 +8,30 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
+// Example for `slog` https://pkg.go.dev/log/slog
+// 	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
+//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
+//		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 {
+//			if v.Error == nil {
+//				logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
+//					slog.String("uri", v.URI),
+//					slog.Int("status", v.Status),
+//				)
+//			} else {
+//				logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
+//					slog.String("uri", v.URI),
+//					slog.Int("status", v.Status),
+//					slog.String("err", v.Error.Error()),
+//				)
+//			}
+//			return nil
+//		},
+//	}))
+//
 // Example for `fmt.Printf`
 // 	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
 //		LogStatus:   true,
@@ -225,7 +249,7 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
 	if config.Skipper == nil {
 		config.Skipper = DefaultSkipper
 	}
-	now = time.Now
+	now := time.Now
 	if config.timeNow != nil {
 		now = config.timeNow
 	}
diff --git a/middleware/rewrite.go b/middleware/rewrite.go
index e5b0a6b..2090eac 100644
--- a/middleware/rewrite.go
+++ b/middleware/rewrite.go
@@ -27,7 +27,7 @@ type (
 		// Example:
 		// "^/old/[0.9]+/":     "/new",
 		// "^/api/.+?/(.*)":     "/v2/$1",
-		RegexRules map[*regexp.Regexp]string `yaml:"regex_rules"`
+		RegexRules map[*regexp.Regexp]string `yaml:"-"`
 	}
 )
 
diff --git a/middleware/util.go b/middleware/util.go
index ab951a0..4d2d172 100644
--- a/middleware/util.go
+++ b/middleware/util.go
@@ -1,7 +1,11 @@
 package middleware
 
 import (
+	"bufio"
+	"crypto/rand"
+	"io"
 	"strings"
+	"sync"
 )
 
 func matchScheme(domain, pattern string) bool {
@@ -52,3 +56,45 @@ func matchSubdomain(domain, pattern string) bool {
 	}
 	return false
 }
+
+// https://tip.golang.org/doc/go1.19#:~:text=Read%20no%20longer%20buffers%20random%20data%20obtained%20from%20the%20operating%20system%20between%20calls
+var randomReaderPool = sync.Pool{New: func() interface{} {
+	return bufio.NewReader(rand.Reader)
+}}
+
+const randomStringCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+const randomStringCharsetLen = 52 // len(randomStringCharset)
+const randomStringMaxByte = 255 - (256 % randomStringCharsetLen)
+
+func randomString(length uint8) string {
+	reader := randomReaderPool.Get().(*bufio.Reader)
+	defer randomReaderPool.Put(reader)
+
+	b := make([]byte, length)
+	r := make([]byte, length+(length/4)) // perf: avoid read from rand.Reader many times
+	var i uint8 = 0
+
+	// security note:
+	// we can't just simply do b[i]=randomStringCharset[rb%len(randomStringCharset)],
+	// len(len(randomStringCharset)) is 52, and rb is [0, 255], 256 = 52 * 4 + 48.
+	// make the first 48 characters more possibly to be generated then others.
+	// So we have to skip bytes when rb > randomStringMaxByte
+
+	for {
+		_, err := io.ReadFull(reader, r)
+		if err != nil {
+			panic("unexpected error happened when reading from bufio.NewReader(crypto/rand.Reader)")
+		}
+		for _, rb := range r {
+			if rb > randomStringMaxByte {
+				// Skip this number to avoid bias.
+				continue
+			}
+			b[i] = randomStringCharset[rb%randomStringCharsetLen]
+			i++
+			if i == length {
+				return string(b)
+			}
+		}
+	}
+}


Test patch:
diff --git a/bind_test.go b/bind_test.go
index c35283d..cffccfb 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -429,6 +429,109 @@ func TestBindUnsupportedMediaType(t *testing.T) {
 	testBindError(t, strings.NewReader(invalidContent), MIMEApplicationJSON, &json.SyntaxError{})
 }
 
+func TestDefaultBinder_bindDataToMap(t *testing.T) {
+	exampleData := map[string][]string{
+		"multiple": {"1", "2"},
+		"single":   {"3"},
+	}
+
+	t.Run("ok, bind to map[string]string", func(t *testing.T) {
+		dest := map[string]string{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string]string{
+				"multiple": "1",
+				"single":   "3",
+			},
+			dest,
+		)
+	})
+
+	t.Run("ok, bind to map[string]string with nil map", func(t *testing.T) {
+		var dest map[string]string
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string]string{
+				"multiple": "1",
+				"single":   "3",
+			},
+			dest,
+		)
+	})
+
+	t.Run("ok, bind to map[string][]string", func(t *testing.T) {
+		dest := map[string][]string{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string][]string{
+				"multiple": {"1", "2"},
+				"single":   {"3"},
+			},
+			dest,
+		)
+	})
+
+	t.Run("ok, bind to map[string][]string with nil map", func(t *testing.T) {
+		var dest map[string][]string
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string][]string{
+				"multiple": {"1", "2"},
+				"single":   {"3"},
+			},
+			dest,
+		)
+	})
+
+	t.Run("ok, bind to map[string]interface", func(t *testing.T) {
+		dest := map[string]interface{}{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string]interface{}{
+				"multiple": []string{"1", "2"},
+				"single":   []string{"3"},
+			},
+			dest,
+		)
+	})
+
+	t.Run("ok, bind to map[string]interface with nil map", func(t *testing.T) {
+		var dest map[string]interface{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t,
+			map[string]interface{}{
+				"multiple": []string{"1", "2"},
+				"single":   []string{"3"},
+			},
+			dest,
+		)
+	})
+
+	t.Run("ok, bind to map[string]int skips", func(t *testing.T) {
+		dest := map[string]int{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t, map[string]int{}, dest)
+	})
+
+	t.Run("ok, bind to map[string]int skips with nil map", func(t *testing.T) {
+		var dest map[string]int
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t, map[string]int(nil), dest)
+	})
+
+	t.Run("ok, bind to map[string][]int skips", func(t *testing.T) {
+		dest := map[string][]int{}
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t, map[string][]int{}, dest)
+	})
+
+	t.Run("ok, bind to map[string][]int skips with nil map", func(t *testing.T) {
+		var dest map[string][]int
+		assert.NoError(t, new(DefaultBinder).bindData(&dest, exampleData, "param"))
+		assert.Equal(t, map[string][]int(nil), dest)
+	})
+}
+
 func TestBindbindData(t *testing.T) {
 	ts := new(bindTestStruct)
 	b := new(DefaultBinder)
diff --git a/context_test.go b/context_test.go
index 11a63cf..4ca2cc8 100644
--- a/context_test.go
+++ b/context_test.go
@@ -19,7 +19,7 @@ import (
 	"time"
 
 	"github.com/labstack/gommon/log"
-	testify "github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 type (
@@ -85,303 +85,443 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, c Context)
 	return t.templates.ExecuteTemplate(w, name, data)
 }
 
-type responseWriterErr struct {
-}
-
-func (responseWriterErr) Header() http.Header {
-	return http.Header{}
-}
+func TestContextEcho(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+	rec := httptest.NewRecorder()
 
-func (responseWriterErr) Write([]byte) (int, error) {
-	return 0, errors.New("err")
-}
+	c := e.NewContext(req, rec).(*context)
 
-func (responseWriterErr) WriteHeader(statusCode int) {
+	assert.Equal(t, e, c.Echo())
 }
 
-func TestContext(t *testing.T) {
+func TestContextRequest(t *testing.T) {
 	e := New()
 	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
 	rec := httptest.NewRecorder()
+
 	c := e.NewContext(req, rec).(*context)
 
-	assert := testify.New(t)
+	assert.NotNil(t, c.Request())
+	assert.Equal(t, req, c.Request())
+}
 
-	// Echo
-	assert.Equal(e, c.Echo())
+func TestContextResponse(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+	rec := httptest.NewRecorder()
 
-	// Request
-	assert.NotNil(c.Request())
+	c := e.NewContext(req, rec).(*context)
 
-	// Response
-	assert.NotNil(c.Response())
+	assert.NotNil(t, c.Response())
+}
+
+func TestContextRenderTemplate(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+	rec := httptest.NewRecorder()
 
-	//--------
-	// Render
-	//--------
+	c := e.NewContext(req, rec).(*context)
 
 	tmpl := &Template{
 		templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
 	}
 	c.echo.Renderer = tmpl
 	err := c.Render(http.StatusOK, "hello", "Jon Snow")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal("Hello, Jon Snow!", rec.Body.String())
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, "Hello, Jon Snow!", rec.Body.String())
 	}
+}
+
+func TestContextRenderErrorsOnNoRenderer(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+	rec := httptest.NewRecorder()
+
+	c := e.NewContext(req, rec).(*context)
 
 	c.echo.Renderer = nil
-	err = c.Render(http.StatusOK, "hello", "Jon Snow")
-	assert.Error(err)
-
-	// JSON
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.JSON(http.StatusOK, user{1, "Jon Snow"})
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(userJSON+"\n", rec.Body.String())
-	}
-
-	// JSON with "?pretty"
-	req = httptest.NewRequest(http.MethodGet, "/?pretty", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.JSON(http.StatusOK, user{1, "Jon Snow"})
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(userJSONPretty+"\n", rec.Body.String())
-	}
-	req = httptest.NewRequest(http.MethodGet, "/", nil) // reset
-
-	// JSONPretty
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.JSONPretty(http.StatusOK, user{1, "Jon Snow"}, "  ")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(userJSONPretty+"\n", rec.Body.String())
-	}
-
-	// JSON (error)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.JSON(http.StatusOK, make(chan bool))
-	assert.Error(err)
-
-	// JSONP
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
+	assert.Error(t, c.Render(http.StatusOK, "hello", "Jon Snow"))
+}
+
+func TestContextJSON(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.JSON(http.StatusOK, user{1, "Jon Snow"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, userJSON+"\n", rec.Body.String())
+	}
+}
+
+func TestContextJSONErrorsOut(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.JSON(http.StatusOK, make(chan bool))
+	assert.EqualError(t, err, "json: unsupported type: chan bool")
+}
+
+func TestContextJSONPrettyURL(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.JSON(http.StatusOK, user{1, "Jon Snow"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, userJSONPretty+"\n", rec.Body.String())
+	}
+}
+
+func TestContextJSONPretty(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.JSONPretty(http.StatusOK, user{1, "Jon Snow"}, "  ")
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, userJSONPretty+"\n", rec.Body.String())
+	}
+}
+
+func TestContextJSONWithEmptyIntent(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	u := user{1, "Jon Snow"}
+	emptyIndent := ""
+	buf := new(bytes.Buffer)
+
+	enc := json.NewEncoder(buf)
+	enc.SetIndent(emptyIndent, emptyIndent)
+	_ = enc.Encode(u)
+	err := c.json(http.StatusOK, user{1, "Jon Snow"}, emptyIndent)
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, buf.String(), rec.Body.String())
+	}
+}
+
+func TestContextJSONP(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
 	callback := "callback"
-	err = c.JSONP(http.StatusOK, callback, user{1, "Jon Snow"})
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(callback+"("+userJSON+"\n);", rec.Body.String())
-	}
-
-	// XML
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.XML(http.StatusOK, user{1, "Jon Snow"})
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(xml.Header+userXML, rec.Body.String())
-	}
-
-	// XML with "?pretty"
-	req = httptest.NewRequest(http.MethodGet, "/?pretty", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.XML(http.StatusOK, user{1, "Jon Snow"})
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(xml.Header+userXMLPretty, rec.Body.String())
-	}
-	req = httptest.NewRequest(http.MethodGet, "/", nil)
-
-	// XML (error)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.XML(http.StatusOK, make(chan bool))
-	assert.Error(err)
-
-	// XML response write error
-	c = e.NewContext(req, rec).(*context)
-	c.response.Writer = responseWriterErr{}
-	err = c.XML(0, 0)
-	testify.Error(t, err)
-
-	// XMLPretty
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.XMLPretty(http.StatusOK, user{1, "Jon Snow"}, "  ")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(xml.Header+userXMLPretty, rec.Body.String())
-	}
-
-	t.Run("empty indent", func(t *testing.T) {
-		var (
-			u           = user{1, "Jon Snow"}
-			buf         = new(bytes.Buffer)
-			emptyIndent = ""
-		)
-
-		t.Run("json", func(t *testing.T) {
-			buf.Reset()
-			assert := testify.New(t)
-
-			// New JSONBlob with empty indent
-			rec = httptest.NewRecorder()
-			c = e.NewContext(req, rec).(*context)
-			enc := json.NewEncoder(buf)
-			enc.SetIndent(emptyIndent, emptyIndent)
-			err = enc.Encode(u)
-			err = c.json(http.StatusOK, user{1, "Jon Snow"}, emptyIndent)
-			if assert.NoError(err) {
-				assert.Equal(http.StatusOK, rec.Code)
-				assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType))
-				assert.Equal(buf.String(), rec.Body.String())
-			}
-		})
+	err := c.JSONP(http.StatusOK, callback, user{1, "Jon Snow"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, callback+"("+userJSON+"\n);", rec.Body.String())
+	}
+}
 
-		t.Run("xml", func(t *testing.T) {
-			buf.Reset()
-			assert := testify.New(t)
-
-			// New XMLBlob with empty indent
-			rec = httptest.NewRecorder()
-			c = e.NewContext(req, rec).(*context)
-			enc := xml.NewEncoder(buf)
-			enc.Indent(emptyIndent, emptyIndent)
-			err = enc.Encode(u)
-			err = c.xml(http.StatusOK, user{1, "Jon Snow"}, emptyIndent)
-			if assert.NoError(err) {
-				assert.Equal(http.StatusOK, rec.Code)
-				assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
-				assert.Equal(xml.Header+buf.String(), rec.Body.String())
-			}
-		})
-	})
+func TestContextJSONBlob(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
 
-	// Legacy JSONBlob
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
 	data, err := json.Marshal(user{1, "Jon Snow"})
-	assert.NoError(err)
+	assert.NoError(t, err)
 	err = c.JSONBlob(http.StatusOK, data)
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(userJSON, rec.Body.String())
-	}
-
-	// Legacy JSONPBlob
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	callback = "callback"
-	data, err = json.Marshal(user{1, "Jon Snow"})
-	assert.NoError(err)
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, userJSON, rec.Body.String())
+	}
+}
+
+func TestContextJSONPBlob(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	callback := "callback"
+	data, err := json.Marshal(user{1, "Jon Snow"})
+	assert.NoError(t, err)
 	err = c.JSONPBlob(http.StatusOK, callback, data)
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(callback+"("+userJSON+");", rec.Body.String())
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationJavaScriptCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, callback+"("+userJSON+");", rec.Body.String())
+	}
+}
+
+func TestContextXML(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.XML(http.StatusOK, user{1, "Jon Snow"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, xml.Header+userXML, rec.Body.String())
+	}
+}
+
+func TestContextXMLPrettyURL(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.XML(http.StatusOK, user{1, "Jon Snow"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, xml.Header+userXMLPretty, rec.Body.String())
 	}
+}
 
-	// Legacy XMLBlob
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	data, err = xml.Marshal(user{1, "Jon Snow"})
-	assert.NoError(err)
+func TestContextXMLPretty(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.XMLPretty(http.StatusOK, user{1, "Jon Snow"}, "  ")
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, xml.Header+userXMLPretty, rec.Body.String())
+	}
+}
+
+func TestContextXMLBlob(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	data, err := xml.Marshal(user{1, "Jon Snow"})
+	assert.NoError(t, err)
 	err = c.XMLBlob(http.StatusOK, data)
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(xml.Header+userXML, rec.Body.String())
-	}
-
-	// String
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.String(http.StatusOK, "Hello, World!")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMETextPlainCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal("Hello, World!", rec.Body.String())
-	}
-
-	// HTML
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.HTML(http.StatusOK, "Hello, <strong>World!</strong>")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal(MIMETextHTMLCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal("Hello, <strong>World!</strong>", rec.Body.String())
-	}
-
-	// Stream
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, xml.Header+userXML, rec.Body.String())
+	}
+}
+
+func TestContextXMLWithEmptyIntent(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	u := user{1, "Jon Snow"}
+	emptyIndent := ""
+	buf := new(bytes.Buffer)
+
+	enc := xml.NewEncoder(buf)
+	enc.Indent(emptyIndent, emptyIndent)
+	_ = enc.Encode(u)
+	err := c.xml(http.StatusOK, user{1, "Jon Snow"}, emptyIndent)
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, xml.Header+buf.String(), rec.Body.String())
+	}
+}
+
+type responseWriterErr struct {
+}
+
+func (responseWriterErr) Header() http.Header {
+	return http.Header{}
+}
+
+func (responseWriterErr) Write([]byte) (int, error) {
+	return 0, errors.New("responseWriterErr")
+}
+
+func (responseWriterErr) WriteHeader(statusCode int) {
+}
+
+func TestContextXMLError(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+	c.response.Writer = responseWriterErr{}
+
+	err := c.XML(http.StatusOK, make(chan bool))
+	assert.EqualError(t, err, "responseWriterErr")
+}
+
+func TestContextString(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.String(http.StatusOK, "Hello, World!")
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMETextPlainCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, "Hello, World!", rec.Body.String())
+	}
+}
+
+func TestContextHTML(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+
+	err := c.HTML(http.StatusOK, "Hello, <strong>World!</strong>")
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, MIMETextHTMLCharsetUTF8, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, "Hello, <strong>World!</strong>", rec.Body.String())
+	}
+}
+
+func TestContextStream(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+
 	r := strings.NewReader("response from a stream")
-	err = c.Stream(http.StatusOK, "application/octet-stream", r)
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal("application/octet-stream", rec.Header().Get(HeaderContentType))
-		assert.Equal("response from a stream", rec.Body.String())
-	}
-
-	// Attachment
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.Attachment("_fixture/images/walle.png", "walle.png")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal("attachment; filename=\"walle.png\"", rec.Header().Get(HeaderContentDisposition))
-		assert.Equal(219885, rec.Body.Len())
-	}
-
-	// Inline
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
-	err = c.Inline("_fixture/images/walle.png", "walle.png")
-	if assert.NoError(err) {
-		assert.Equal(http.StatusOK, rec.Code)
-		assert.Equal("inline; filename=\"walle.png\"", rec.Header().Get(HeaderContentDisposition))
-		assert.Equal(219885, rec.Body.Len())
-	}
-
-	// NoContent
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
+	err := c.Stream(http.StatusOK, "application/octet-stream", r)
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.Equal(t, "application/octet-stream", rec.Header().Get(HeaderContentType))
+		assert.Equal(t, "response from a stream", rec.Body.String())
+	}
+}
+
+func TestContextAttachment(t *testing.T) {
+	var testCases = []struct {
+		name         string
+		whenName     string
+		expectHeader string
+	}{
+		{
+			name:         "ok",
+			whenName:     "walle.png",
+			expectHeader: `attachment; filename="walle.png"`,
+		},
+		{
+			name:         "ok, escape quotes in malicious filename",
+			whenName:     `malicious.sh"; \"; dummy=.txt`,
+			expectHeader: `attachment; filename="malicious.sh\"; \\\"; dummy=.txt"`,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := New()
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest(http.MethodGet, "/", nil)
+			c := e.NewContext(req, rec).(*context)
+
+			err := c.Attachment("_fixture/images/walle.png", tc.whenName)
+			if assert.NoError(t, err) {
+				assert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))
+
+				assert.Equal(t, http.StatusOK, rec.Code)
+				assert.Equal(t, 219885, rec.Body.Len())
+			}
+		})
+	}
+}
+
+func TestContextInline(t *testing.T) {
+	var testCases = []struct {
+		name         string
+		whenName     string
+		expectHeader string
+	}{
+		{
+			name:         "ok",
+			whenName:     "walle.png",
+			expectHeader: `inline; filename="walle.png"`,
+		},
+		{
+			name:         "ok, escape quotes in malicious filename",
+			whenName:     `malicious.sh"; \"; dummy=.txt`,
+			expectHeader: `inline; filename="malicious.sh\"; \\\"; dummy=.txt"`,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := New()
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest(http.MethodGet, "/", nil)
+			c := e.NewContext(req, rec).(*context)
+
+			err := c.Inline("_fixture/images/walle.png", tc.whenName)
+			if assert.NoError(t, err) {
+				assert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))
+
+				assert.Equal(t, http.StatusOK, rec.Code)
+				assert.Equal(t, 219885, rec.Body.Len())
+			}
+		})
+	}
+}
+
+func TestContextNoContent(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
+
 	c.NoContent(http.StatusOK)
-	assert.Equal(http.StatusOK, rec.Code)
+	assert.Equal(t, http.StatusOK, rec.Code)
+}
+
+func TestContextError(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
+	c := e.NewContext(req, rec).(*context)
 
-	// Error
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec).(*context)
 	c.Error(errors.New("error"))
-	assert.Equal(http.StatusInternalServerError, rec.Code)
+	assert.Equal(t, http.StatusInternalServerError, rec.Code)
+	assert.True(t, c.Response().Committed)
+}
+
+func TestContextReset(t *testing.T) {
+	e := New()
+	rec := httptest.NewRecorder()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	c := e.NewContext(req, rec).(*context)
 
-	// Reset
 	c.SetParamNames("foo")
 	c.SetParamValues("bar")
 	c.Set("foe", "ban")
 	c.query = url.Values(map[string][]string{"fon": {"baz"}})
+
 	c.Reset(req, httptest.NewRecorder())
-	assert.Equal(0, len(c.ParamValues()))
-	assert.Equal(0, len(c.ParamNames()))
-	assert.Equal(0, len(c.store))
-	assert.Equal("", c.Path())
-	assert.Equal(0, len(c.QueryParams()))
+
+	assert.Len(t, c.ParamValues(), 0)
+	assert.Len(t, c.ParamNames(), 0)
+	assert.Len(t, c.Path(), 0)
+	assert.Len(t, c.QueryParams(), 0)
+	assert.Len(t, c.store, 0)
 }
 
 func TestContext_JSON_CommitsCustomResponseCode(t *testing.T) {
@@ -391,11 +531,10 @@ func TestContext_JSON_CommitsCustomResponseCode(t *testing.T) {
 	c := e.NewContext(req, rec).(*context)
 	err := c.JSON(http.StatusCreated, user{1, "Jon Snow"})
 
-	assert := testify.New(t)
-	if assert.NoError(err) {
-		assert.Equal(http.StatusCreated, rec.Code)
-		assert.Equal(MIMEApplicationJSONCharsetUTF8, rec.Header().Get(HeaderContentType))
-		assert.Equal(userJSON+"\n", rec.Body.String())
+	if assert.NoError(t, err) {
+		assert.Equal(t, http.StatusCreated, rec.Code)
+		assert.Equal(t, MIMEApplicationJSON, rec.Header().Get(HeaderContentType))
+		assert.Equal(t, userJSON+"\n", rec.Body.String())
 	}
 }
 
@@ -406,9 +545,8 @@ func TestContext_JSON_DoesntCommitResponseCodePrematurely(t *testing.T) {
 	c := e.NewContext(req, rec).(*context)
 	err := c.JSON(http.StatusCreated, map[string]float64{"a": math.NaN()})
 
-	assert := testify.New(t)
-	if assert.Error(err) {
-		assert.False(c.response.Committed)
+	if assert.Error(t, err) {
+		assert.False(t, c.response.Committed)
 	}
 }
 
@@ -422,22 +560,20 @@ func TestContextCookie(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec).(*context)
 
-	assert := testify.New(t)
-
 	// Read single
 	cookie, err := c.Cookie("theme")
-	if assert.NoError(err) {
-		assert.Equal("theme", cookie.Name)
-		assert.Equal("light", cookie.Value)
+	if assert.NoError(t, err) {
+		assert.Equal(t, "theme", cookie.Name)
+		assert.Equal(t, "light", cookie.Value)
 	}
 
 	// Read multiple
 	for _, cookie := range c.Cookies() {
 		switch cookie.Name {
 		case "theme":
-			assert.Equal("light", cookie.Value)
+			assert.Equal(t, "light", cookie.Value)
 		case "user":
-			assert.Equal("Jon Snow", cookie.Value)
+			assert.Equal(t, "Jon Snow", cookie.Value)
 		}
 	}
 
@@ -452,11 +588,11 @@ func TestContextCookie(t *testing.T) {
 		HttpOnly: true,
 	}
 	c.SetCookie(cookie)
-	assert.Contains(rec.Header().Get(HeaderSetCookie), "SSID")
-	assert.Contains(rec.Header().Get(HeaderSetCookie), "Ap4PGTEq")
-	assert.Contains(rec.Header().Get(HeaderSetCookie), "labstack.com")
-	assert.Contains(rec.Header().Get(HeaderSetCookie), "Secure")
-	assert.Contains(rec.Header().Get(HeaderSetCookie), "HttpOnly")
+	assert.Contains(t, rec.Header().Get(HeaderSetCookie), "SSID")
+	assert.Contains(t, rec.Header().Get(HeaderSetCookie), "Ap4PGTEq")
+	assert.Contains(t, rec.Header().Get(HeaderSetCookie), "labstack.com")
+	assert.Contains(t, rec.Header().Get(HeaderSetCookie), "Secure")
+	assert.Contains(t, rec.Header().Get(HeaderSetCookie), "HttpOnly")
 }
 
 func TestContextPath(t *testing.T) {
@@ -469,14 +605,12 @@ func TestContextPath(t *testing.T) {
 	c := e.NewContext(nil, nil)
 	r.Find(http.MethodGet, "/users/1", c)
 
-	assert := testify.New(t)
-
-	assert.Equal("/users/:id", c.Path())
+	assert.Equal(t, "/users/:id", c.Path())
 
 	r.Add(http.MethodGet, "/users/:uid/files/:fid", handler)
 	c = e.NewContext(nil, nil)
 	r.Find(http.MethodGet, "/users/1/files/1", c)
-	assert.Equal("/users/:uid/files/:fid", c.Path())
+	assert.Equal(t, "/users/:uid/files/:fid", c.Path())
 }
 
 func TestContextPathParam(t *testing.T) {
@@ -486,15 +620,15 @@ func TestContextPathParam(t *testing.T) {
 
 	// ParamNames
 	c.SetParamNames("uid", "fid")
-	testify.EqualValues(t, []string{"uid", "fid"}, c.ParamNames())
+	assert.EqualValues(t, []string{"uid", "fid"}, c.ParamNames())
 
 	// ParamValues
 	c.SetParamValues("101", "501")
-	testify.EqualValues(t, []string{"101", "501"}, c.ParamValues())
+	assert.EqualValues(t, []string{"101", "501"}, c.ParamValues())
 
 	// Param
-	testify.Equal(t, "501", c.Param("fid"))
-	testify.Equal(t, "", c.Param("undefined"))
+	assert.Equal(t, "501", c.Param("fid"))
+	assert.Equal(t, "", c.Param("undefined"))
 }
 
 func TestContextGetAndSetParam(t *testing.T) {
@@ -507,23 +641,21 @@ func TestContextGetAndSetParam(t *testing.T) {
 
 	// round-trip param values with modification
 	paramVals := c.ParamValues()
-	testify.EqualValues(t, []string{""}, c.ParamValues())
+	assert.EqualValues(t, []string{""}, c.ParamValues())
 	paramVals[0] = "bar"
 	c.SetParamValues(paramVals...)
-	testify.EqualValues(t, []string{"bar"}, c.ParamValues())
+	assert.EqualValues(t, []string{"bar"}, c.ParamValues())
 
 	// shouldn't explode during Reset() afterwards!
-	testify.NotPanics(t, func() {
+	assert.NotPanics(t, func() {
 		c.Reset(nil, nil)
 	})
 }
 
 // Issue #1655
 func TestContextSetParamNamesShouldUpdateEchoMaxParam(t *testing.T) {
-	assert := testify.New(t)
-
 	e := New()
-	assert.Equal(0, *e.maxParam)
+	assert.Equal(t, 0, *e.maxParam)
 
 	expectedOneParam := []string{"one"}
 	expectedTwoParams := []string{"one", "two"}
@@ -533,23 +665,23 @@ func TestContextSetParamNamesShouldUpdateEchoMaxParam(t *testing.T) {
 	c := e.NewContext(nil, nil)
 	c.SetParamNames("1", "2")
 	c.SetParamValues(expectedTwoParams...)
-	assert.Equal(2, *e.maxParam)
-	assert.EqualValues(expectedTwoParams, c.ParamValues())
+	assert.Equal(t, 2, *e.maxParam)
+	assert.EqualValues(t, expectedTwoParams, c.ParamValues())
 
 	c.SetParamNames("1")
-	assert.Equal(2, *e.maxParam)
+	assert.Equal(t, 2, *e.maxParam)
 	// Here for backward compatibility the ParamValues remains as they are
-	assert.EqualValues(expectedOneParam, c.ParamValues())
+	assert.EqualValues(t, expectedOneParam, c.ParamValues())
 
 	c.SetParamNames("1", "2", "3")
-	assert.Equal(3, *e.maxParam)
+	assert.Equal(t, 3, *e.maxParam)
 	// Here for backward compatibility the ParamValues remains as they are, but the len is extended to e.maxParam
-	assert.EqualValues(expectedThreeParams, c.ParamValues())
+	assert.EqualValues(t, expectedThreeParams, c.ParamValues())
 
 	c.SetParamValues("A", "B", "C", "D")
-	assert.Equal(3, *e.maxParam)
+	assert.Equal(t, 3, *e.maxParam)
 	// Here D shouldn't be returned
-	assert.EqualValues(expectedABCParams, c.ParamValues())
+	assert.EqualValues(t, expectedABCParams, c.ParamValues())
 }
 
 func TestContextFormValue(t *testing.T) {
@@ -563,13 +695,13 @@ func TestContextFormValue(t *testing.T) {
 	c := e.NewContext(req, nil)
 
 	// FormValue
-	testify.Equal(t, "Jon Snow", c.FormValue("name"))
-	testify.Equal(t, "jon@labstack.com", c.FormValue("email"))
+	assert.Equal(t, "Jon Snow", c.FormValue("name"))
+	assert.Equal(t, "jon@labstack.com", c.FormValue("email"))
 
 	// FormParams
 	params, err := c.FormParams()
-	if testify.NoError(t, err) {
-		testify.Equal(t, url.Values{
+	if assert.NoError(t, err) {
+		assert.Equal(t, url.Values{
 			"name":  []string{"Jon Snow"},
 			"email": []string{"jon@labstack.com"},
 		}, params)
@@ -580,8 +712,8 @@ func TestContextFormValue(t *testing.T) {
 	req.Header.Add(HeaderContentType, MIMEMultipartForm)
 	c = e.NewContext(req, nil)
 	params, err = c.FormParams()
-	testify.Nil(t, params)
-	testify.Error(t, err)
+	assert.Nil(t, params)
+	assert.Error(t, err)
 }
 
 func TestContextQueryParam(t *testing.T) {
@@ -593,11 +725,11 @@ func TestContextQueryParam(t *testing.T) {
 	c := e.NewContext(req, nil)
 
 	// QueryParam
-	testify.Equal(t, "Jon Snow", c.QueryParam("name"))
-	testify.Equal(t, "jon@labstack.com", c.QueryParam("email"))
+	assert.Equal(t, "Jon Snow", c.QueryParam("name"))
+	assert.Equal(t, "jon@labstack.com", c.QueryParam("email"))
 
 	// QueryParams
-	testify.Equal(t, url.Values{
+	assert.Equal(t, url.Values{
 		"name":  []string{"Jon Snow"},
 		"email": []string{"jon@labstack.com"},
 	}, c.QueryParams())
@@ -608,7 +740,7 @@ func TestContextFormFile(t *testing.T) {
 	buf := new(bytes.Buffer)
 	mr := multipart.NewWriter(buf)
 	w, err := mr.CreateFormFile("file", "test")
-	if testify.NoError(t, err) {
+	if assert.NoError(t, err) {
 		w.Write([]byte("test"))
 	}
 	mr.Close()
@@ -617,8 +749,8 @@ func TestContextFormFile(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	f, err := c.FormFile("file")
-	if testify.NoError(t, err) {
-		testify.Equal(t, "test", f.Filename)
+	if assert.NoError(t, err) {
+		assert.Equal(t, "test", f.Filename)
 	}
 }
 
@@ -633,8 +765,8 @@ func TestContextMultipartForm(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	f, err := c.MultipartForm()
-	if testify.NoError(t, err) {
-		testify.NotNil(t, f)
+	if assert.NoError(t, err) {
+		assert.NotNil(t, f)
 	}
 }
 
@@ -643,16 +775,16 @@ func TestContextRedirect(t *testing.T) {
 	req := httptest.NewRequest(http.MethodGet, "/", nil)
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
-	testify.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo"))
-	testify.Equal(t, http.StatusMovedPermanently, rec.Code)
-	testify.Equal(t, "http://labstack.github.io/echo", rec.Header().Get(HeaderLocation))
-	testify.Error(t, c.Redirect(310, "http://labstack.github.io/echo"))
+	assert.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo"))
+	assert.Equal(t, http.StatusMovedPermanently, rec.Code)
+	assert.Equal(t, "http://labstack.github.io/echo", rec.Header().Get(HeaderLocation))
+	assert.Error(t, c.Redirect(310, "http://labstack.github.io/echo"))
 }
 
 func TestContextStore(t *testing.T) {
 	var c Context = new(context)
 	c.Set("name", "Jon Snow")
-	testify.Equal(t, "Jon Snow", c.Get("name"))
+	assert.Equal(t, "Jon Snow", c.Get("name"))
 }
 
 func BenchmarkContext_Store(b *testing.B) {
@@ -682,19 +814,19 @@ func TestContextHandler(t *testing.T) {
 	c := e.NewContext(nil, nil)
 	r.Find(http.MethodGet, "/handler", c)
 	err := c.Handler()(c)
-	testify.Equal(t, "handler", b.String())
-	testify.NoError(t, err)
+	assert.Equal(t, "handler", b.String())
+	assert.NoError(t, err)
 }
 
 func TestContext_SetHandler(t *testing.T) {
 	var c Context = new(context)
 
-	testify.Nil(t, c.Handler())
+	assert.Nil(t, c.Handler())
 
 	c.SetHandler(func(c Context) error {
 		return nil
 	})
-	testify.NotNil(t, c.Handler())
+	assert.NotNil(t, c.Handler())
 }
 
 func TestContext_Path(t *testing.T) {
@@ -703,7 +835,7 @@ func TestContext_Path(t *testing.T) {
 	var c Context = new(context)
 
 	c.SetPath(path)
-	testify.Equal(t, path, c.Path())
+	assert.Equal(t, path, c.Path())
 }
 
 type validator struct{}
@@ -716,10 +848,10 @@ func TestContext_Validate(t *testing.T) {
 	e := New()
 	c := e.NewContext(nil, nil)
 
-	testify.Error(t, c.Validate(struct{}{}))
+	assert.Error(t, c.Validate(struct{}{}))
 
 	e.Validator = &validator{}
-	testify.NoError(t, c.Validate(struct{}{}))
+	assert.NoError(t, c.Validate(struct{}{}))
 }
 
 func TestContext_QueryString(t *testing.T) {
@@ -730,18 +862,18 @@ func TestContext_QueryString(t *testing.T) {
 	req := httptest.NewRequest(http.MethodGet, "/?"+queryString, nil)
 	c := e.NewContext(req, nil)
 
-	testify.Equal(t, queryString, c.QueryString())
+	assert.Equal(t, queryString, c.QueryString())
 }
 
 func TestContext_Request(t *testing.T) {
 	var c Context = new(context)
 
-	testify.Nil(t, c.Request())
+	assert.Nil(t, c.Request())
 
 	req := httptest.NewRequest(http.MethodGet, "/path", nil)
 	c.SetRequest(req)
 
-	testify.Equal(t, req, c.Request())
+	assert.Equal(t, req, c.Request())
 }
 
 func TestContext_Scheme(t *testing.T) {
@@ -798,14 +930,14 @@ func TestContext_Scheme(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		testify.Equal(t, tt.s, tt.c.Scheme())
+		assert.Equal(t, tt.s, tt.c.Scheme())
 	}
 }
 
 func TestContext_IsWebSocket(t *testing.T) {
 	tests := []struct {
 		c  Context
-		ws testify.BoolAssertionFunc
+		ws assert.BoolAssertionFunc
 	}{
 		{
 			&context{
@@ -813,7 +945,7 @@ func TestContext_IsWebSocket(t *testing.T) {
 					Header: http.Header{HeaderUpgrade: []string{"websocket"}},
 				},
 			},
-			testify.True,
+			assert.True,
 		},
 		{
 			&context{
@@ -821,13 +953,13 @@ func TestContext_IsWebSocket(t *testing.T) {
 					Header: http.Header{HeaderUpgrade: []string{"Websocket"}},
 				},
 			},
-			testify.True,
+			assert.True,
 		},
 		{
 			&context{
 				request: &http.Request{},
 			},
-			testify.False,
+			assert.False,
 		},
 		{
 			&context{
@@ -835,7 +967,7 @@ func TestContext_IsWebSocket(t *testing.T) {
 					Header: http.Header{HeaderUpgrade: []string{"other"}},
 				},
 			},
-			testify.False,
+			assert.False,
 		},
 	}
 
@@ -854,8 +986,8 @@ func TestContext_Bind(t *testing.T) {
 
 	req.Header.Add(HeaderContentType, MIMEApplicationJSON)
 	err := c.Bind(u)
-	testify.NoError(t, err)
-	testify.Equal(t, &user{1, "Jon Snow"}, u)
+	assert.NoError(t, err)
+	assert.Equal(t, &user{1, "Jon Snow"}, u)
 }
 
 func TestContext_Logger(t *testing.T) {
@@ -863,15 +995,15 @@ func TestContext_Logger(t *testing.T) {
 	c := e.NewContext(nil, nil)
 
 	log1 := c.Logger()
-	testify.NotNil(t, log1)
+	assert.NotNil(t, log1)
 
 	log2 := log.New("echo2")
 	c.SetLogger(log2)
-	testify.Equal(t, log2, c.Logger())
+	assert.Equal(t, log2, c.Logger())
 
 	// Resetting the context returns the initial logger
 	c.Reset(nil, nil)
-	testify.Equal(t, log1, c.Logger())
+	assert.Equal(t, log1, c.Logger())
 }
 
 func TestContext_RealIP(t *testing.T) {
@@ -959,6 +1091,6 @@ func TestContext_RealIP(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		testify.Equal(t, tt.s, tt.c.RealIP())
+		assert.Equal(t, tt.s, tt.c.RealIP())
 	}
 }
diff --git a/echo_test.go b/echo_test.go
index a352e40..4164791 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -1572,7 +1572,7 @@ func TestEcho_OnAddRouteHandler(t *testing.T) {
 		})
 	}
 
-	e.GET("/static", NotFoundHandler)
+	e.GET("/static", dummyHandler)
 	e.Host("domain.site").GET("/static/*", dummyHandler, func(next HandlerFunc) HandlerFunc {
 		return func(c Context) error {
 			return next(c)
@@ -1582,7 +1582,7 @@ func TestEcho_OnAddRouteHandler(t *testing.T) {
 	assert.Len(t, added, 2)
 
 	assert.Equal(t, "", added[0].host)
-	assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.glob..func1"}, added[0].route)
+	assert.Equal(t, Route{Method: http.MethodGet, Path: "/static", Name: "github.com/labstack/echo/v4.TestEcho_OnAddRouteHandler.func1"}, added[0].route)
 	assert.Len(t, added[0].middleware, 0)
 
 	assert.Equal(t, "domain.site", added[1].host)
diff --git a/json_test.go b/json_test.go
index 27ee43e..8fb9ebc 100644
--- a/json_test.go
+++ b/json_test.go
@@ -1,7 +1,7 @@
 package echo
 
 import (
-	testify "github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/assert"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -16,16 +16,14 @@ func TestDefaultJSONCodec_Encode(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec).(*context)
 
-	assert := testify.New(t)
-
 	// Echo
-	assert.Equal(e, c.Echo())
+	assert.Equal(t, e, c.Echo())
 
 	// Request
-	assert.NotNil(c.Request())
+	assert.NotNil(t, c.Request())
 
 	// Response
-	assert.NotNil(c.Response())
+	assert.NotNil(t, c.Response())
 
 	//--------
 	// Default JSON encoder
@@ -34,16 +32,16 @@ func TestDefaultJSONCodec_Encode(t *testing.T) {
 	enc := new(DefaultJSONSerializer)
 
 	err := enc.Serialize(c, user{1, "Jon Snow"}, "")
-	if assert.NoError(err) {
-		assert.Equal(userJSON+"\n", rec.Body.String())
+	if assert.NoError(t, err) {
+		assert.Equal(t, userJSON+"\n", rec.Body.String())
 	}
 
 	req = httptest.NewRequest(http.MethodPost, "/", nil)
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec).(*context)
 	err = enc.Serialize(c, user{1, "Jon Snow"}, "  ")
-	if assert.NoError(err) {
-		assert.Equal(userJSONPretty+"\n", rec.Body.String())
+	if assert.NoError(t, err) {
+		assert.Equal(t, userJSONPretty+"\n", rec.Body.String())
 	}
 }
 
@@ -55,16 +53,14 @@ func TestDefaultJSONCodec_Decode(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec).(*context)
 
-	assert := testify.New(t)
-
 	// Echo
-	assert.Equal(e, c.Echo())
+	assert.Equal(t, e, c.Echo())
 
 	// Request
-	assert.NotNil(c.Request())
+	assert.NotNil(t, c.Request())
 
 	// Response
-	assert.NotNil(c.Response())
+	assert.NotNil(t, c.Response())
 
 	//--------
 	// Default JSON encoder
@@ -74,8 +70,8 @@ func TestDefaultJSONCodec_Decode(t *testing.T) {
 
 	var u = user{}
 	err := enc.Deserialize(c, &u)
-	if assert.NoError(err) {
-		assert.Equal(u, user{ID: 1, Name: "Jon Snow"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, u, user{ID: 1, Name: "Jon Snow"})
 	}
 
 	var userUnmarshalSyntaxError = user{}
@@ -83,8 +79,8 @@ func TestDefaultJSONCodec_Decode(t *testing.T) {
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec).(*context)
 	err = enc.Deserialize(c, &userUnmarshalSyntaxError)
-	assert.IsType(&HTTPError{}, err)
-	assert.EqualError(err, "code=400, message=Syntax error: offset=1, error=invalid character 'i' looking for beginning of value, internal=invalid character 'i' looking for beginning of value")
+	assert.IsType(t, &HTTPError{}, err)
+	assert.EqualError(t, err, "code=400, message=Syntax error: offset=1, error=invalid character 'i' looking for beginning of value, internal=invalid character 'i' looking for beginning of value")
 
 	var userUnmarshalTypeError = struct {
 		ID   string `json:"id"`
@@ -95,7 +91,7 @@ func TestDefaultJSONCodec_Decode(t *testing.T) {
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec).(*context)
 	err = enc.Deserialize(c, &userUnmarshalTypeError)
-	assert.IsType(&HTTPError{}, err)
-	assert.EqualError(err, "code=400, message=Unmarshal type error: expected=string, got=number, field=id, offset=7, internal=json: cannot unmarshal number into Go struct field .id of type string")
+	assert.IsType(t, &HTTPError{}, err)
+	assert.EqualError(t, err, "code=400, message=Unmarshal type error: expected=string, got=number, field=id, offset=7, internal=json: cannot unmarshal number into Go struct field .id of type string")
 
 }
diff --git a/middleware/basic_auth_test.go b/middleware/basic_auth_test.go
index 20e7692..2e133e0 100644
--- a/middleware/basic_auth_test.go
+++ b/middleware/basic_auth_test.go
@@ -32,7 +32,6 @@ func TestBasicAuth(t *testing.T) {
 	assert.NoError(t, h(c))
 
 	h = BasicAuthWithConfig(BasicAuthConfig{
-		Skipper:   nil,
 		Validator: f,
 		Realm:     "someRealm",
 	})(func(c echo.Context) error {
@@ -72,4 +71,20 @@ func TestBasicAuth(t *testing.T) {
 	req.Header.Set(echo.HeaderAuthorization, auth)
 	he = h(c).(*echo.HTTPError)
 	assert.Equal(t, http.StatusUnauthorized, he.Code)
+
+	h = BasicAuthWithConfig(BasicAuthConfig{
+		Validator: f,
+		Realm:     "someRealm",
+		Skipper: func(c echo.Context) bool {
+			return true
+		},
+	})(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	// Skipped Request
+	auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("joe:skip"))
+	req.Header.Set(echo.HeaderAuthorization, auth)
+	assert.NoError(t, h(c))
+
 }
diff --git a/middleware/body_limit_test.go b/middleware/body_limit_test.go
index 2bfce37..0fd66ee 100644
--- a/middleware/body_limit_test.go
+++ b/middleware/body_limit_test.go
@@ -56,9 +56,6 @@ func TestBodyLimit(t *testing.T) {
 
 func TestBodyLimitReader(t *testing.T) {
 	hw := []byte("Hello, World!")
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(hw))
-	rec := httptest.NewRecorder()
 
 	config := BodyLimitConfig{
 		Skipper: DefaultSkipper,
@@ -68,7 +65,6 @@ func TestBodyLimitReader(t *testing.T) {
 	reader := &limitedReader{
 		BodyLimitConfig: config,
 		reader:          io.NopCloser(bytes.NewReader(hw)),
-		context:         e.NewContext(req, rec),
 	}
 
 	// read all should return ErrStatusRequestEntityTooLarge
@@ -78,7 +74,7 @@ func TestBodyLimitReader(t *testing.T) {
 
 	// reset reader and read two bytes must succeed
 	bt := make([]byte, 2)
-	reader.Reset(io.NopCloser(bytes.NewReader(hw)), e.NewContext(req, rec))
+	reader.Reset(io.NopCloser(bytes.NewReader(hw)))
 	n, err := reader.Read(bt)
 	assert.Equal(t, 2, n)
 	assert.Equal(t, nil, err)
diff --git a/middleware/compress_test.go b/middleware/compress_test.go
index e43e2d6..0ed16c8 100644
--- a/middleware/compress_test.go
+++ b/middleware/compress_test.go
@@ -89,8 +89,6 @@ func TestGzip(t *testing.T) {
 }
 
 func TestGzipWithMinLength(t *testing.T) {
-	assert := assert.New(t)
-
 	e := echo.New()
 	// Minimal response length
 	e.Use(GzipWithConfig(GzipConfig{MinLength: 10}))
@@ -103,19 +101,17 @@ func TestGzipWithMinLength(t *testing.T) {
 	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
 	rec := httptest.NewRecorder()
 	e.ServeHTTP(rec, req)
-	assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+	assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
 	r, err := gzip.NewReader(rec.Body)
-	if assert.NoError(err) {
+	if assert.NoError(t, err) {
 		buf := new(bytes.Buffer)
 		defer r.Close()
 		buf.ReadFrom(r)
-		assert.Equal("foobarfoobar", buf.String())
+		assert.Equal(t, "foobarfoobar", buf.String())
 	}
 }
 
 func TestGzipWithMinLengthTooShort(t *testing.T) {
-	assert := assert.New(t)
-
 	e := echo.New()
 	// Minimal response length
 	e.Use(GzipWithConfig(GzipConfig{MinLength: 10}))
@@ -127,13 +123,29 @@ func TestGzipWithMinLengthTooShort(t *testing.T) {
 	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
 	rec := httptest.NewRecorder()
 	e.ServeHTTP(rec, req)
-	assert.Equal("", rec.Header().Get(echo.HeaderContentEncoding))
-	assert.Contains(rec.Body.String(), "test")
+	assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding))
+	assert.Contains(t, rec.Body.String(), "test")
 }
 
-func TestGzipWithMinLengthChunked(t *testing.T) {
-	assert := assert.New(t)
+func TestGzipWithResponseWithoutBody(t *testing.T) {
+	e := echo.New()
+
+	e.Use(Gzip())
+	e.GET("/", func(c echo.Context) error {
+		return c.Redirect(http.StatusMovedPermanently, "http://localhost")
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	req.Header.Set(echo.HeaderAcceptEncoding, gzipScheme)
+	rec := httptest.NewRecorder()
 
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusMovedPermanently, rec.Code)
+	assert.Equal(t, "", rec.Header().Get(echo.HeaderContentEncoding))
+}
+
+func TestGzipWithMinLengthChunked(t *testing.T) {
 	e := echo.New()
 
 	// Gzip chunked
@@ -155,36 +167,36 @@ func TestGzipWithMinLengthChunked(t *testing.T) {
 		c.Response().Flush()
 
 		// Read the first part of the data
-		assert.True(rec.Flushed)
-		assert.Equal(gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
+		assert.True(t, rec.Flushed)
+		assert.Equal(t, gzipScheme, rec.Header().Get(echo.HeaderContentEncoding))
 
 		var err error
 		r, err = gzip.NewReader(rec.Body)
-		assert.NoError(err)
+		assert.NoError(t, err)
 
 		_, err = io.ReadFull(r, chunkBuf)
-		assert.NoError(err)
-		assert.Equal("test\n", string(chunkBuf))
+		assert.NoError(t, err)
+		assert.Equal(t, "test\n", string(chunkBuf))
 
 		// Write and flush the second part of the data
 		c.Response().Write([]byte("test\n"))
 		c.Response().Flush()
 
 		_, err = io.ReadFull(r, chunkBuf)
-		assert.NoError(err)
-		assert.Equal("test\n", string(chunkBuf))
+		assert.NoError(t, err)
+		assert.Equal(t, "test\n", string(chunkBuf))
 
 		// Write the final part of the data and return
 		c.Response().Write([]byte("test"))
 		return nil
 	})(c)
 
-	assert.NotNil(r)
+	assert.NotNil(t, r)
 
 	buf := new(bytes.Buffer)
 
 	buf.ReadFrom(r)
-	assert.Equal("test", buf.String())
+	assert.Equal(t, "test", buf.String())
 
 	r.Close()
 }
diff --git a/middleware/cors_test.go b/middleware/cors_test.go
index c1bb91e..797600c 100644
--- a/middleware/cors_test.go
+++ b/middleware/cors_test.go
@@ -60,6 +60,59 @@ func TestCORS(t *testing.T) {
 				echo.HeaderAccessControlMaxAge:           "3600",
 			},
 		},
+		{
+			name: "ok, preflight request when `Access-Control-Max-Age` is set",
+			givenMW: CORSWithConfig(CORSConfig{
+				AllowOrigins:     []string{"localhost"},
+				AllowCredentials: true,
+				MaxAge:           1,
+			}),
+			whenMethod: http.MethodOptions,
+			whenHeaders: map[string]string{
+				echo.HeaderOrigin:      "localhost",
+				echo.HeaderContentType: echo.MIMEApplicationJSON,
+			},
+			expectHeaders: map[string]string{
+				echo.HeaderAccessControlMaxAge: "1",
+			},
+		},
+		{
+			name: "ok, preflight request when `Access-Control-Max-Age` is set to 0 - not to cache response",
+			givenMW: CORSWithConfig(CORSConfig{
+				AllowOrigins:     []string{"localhost"},
+				AllowCredentials: true,
+				MaxAge:           -1, // forces `Access-Control-Max-Age: 0`
+			}),
+			whenMethod: http.MethodOptions,
+			whenHeaders: map[string]string{
+				echo.HeaderOrigin:      "localhost",
+				echo.HeaderContentType: echo.MIMEApplicationJSON,
+			},
+			expectHeaders: map[string]string{
+				echo.HeaderAccessControlMaxAge: "0",
+			},
+		},
+		{
+			name: "ok, CORS check are skipped",
+			givenMW: CORSWithConfig(CORSConfig{
+				AllowOrigins:     []string{"localhost"},
+				AllowCredentials: true,
+				Skipper: func(c echo.Context) bool {
+					return true
+				},
+			}),
+			whenMethod: http.MethodOptions,
+			whenHeaders: map[string]string{
+				echo.HeaderOrigin:      "localhost",
+				echo.HeaderContentType: echo.MIMEApplicationJSON,
+			},
+			notExpectHeaders: map[string]string{
+				echo.HeaderAccessControlAllowOrigin:      "localhost",
+				echo.HeaderAccessControlAllowMethods:     "GET,HEAD,PUT,PATCH,POST,DELETE",
+				echo.HeaderAccessControlAllowCredentials: "true",
+				echo.HeaderAccessControlMaxAge:           "3600",
+			},
+		},
 		{
 			name: "ok, preflight request with wildcard `AllowOrigins` and `AllowCredentials` true",
 			givenMW: CORSWithConfig(CORSConfig{
diff --git a/middleware/csrf_test.go b/middleware/csrf_test.go
index 6bccdbe..6b20297 100644
--- a/middleware/csrf_test.go
+++ b/middleware/csrf_test.go
@@ -8,7 +8,6 @@ import (
 	"testing"
 
 	"github.com/labstack/echo/v4"
-	"github.com/labstack/gommon/random"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -233,7 +232,7 @@ func TestCSRF(t *testing.T) {
 	assert.Error(t, h(c))
 
 	// Valid CSRF token
-	token := random.String(32)
+	token := randomString(32)
 	req.Header.Set(echo.HeaderCookie, "_csrf="+token)
 	req.Header.Set(echo.HeaderXCSRFToken, token)
 	if assert.NoError(t, h(c)) {
diff --git a/middleware/decompress_test.go b/middleware/decompress_test.go
index 2e73ba8..351e0e7 100644
--- a/middleware/decompress_test.go
+++ b/middleware/decompress_test.go
@@ -131,7 +131,7 @@ func TestDecompressSkipper(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	e.ServeHTTP(rec, req)
-	assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8)
+	assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSON)
 	reqBody, err := io.ReadAll(c.Request().Body)
 	assert.NoError(t, err)
 	assert.Equal(t, body, string(reqBody))
diff --git a/middleware/proxy_test.go b/middleware/proxy_test.go
index 1b5ba6c..1c93ba0 100644
--- a/middleware/proxy_test.go
+++ b/middleware/proxy_test.go
@@ -188,7 +188,7 @@ func TestProxyRealIPHeader(t *testing.T) {
 	tests := []*struct {
 		hasRealIPheader bool
 		hasIPExtractor  bool
-		extectedXRealIP string
+		expectedXRealIP string
 	}{
 		{false, false, remoteAddrIP},
 		{false, true, extractedRealIP},
@@ -210,7 +210,7 @@ func TestProxyRealIPHeader(t *testing.T) {
 			e.IPExtractor = nil
 		}
 		e.ServeHTTP(rec, req)
-		assert.Equal(t, tt.extectedXRealIP, req.Header.Get(echo.HeaderXRealIP), "hasRealIPheader: %t / hasIPExtractor: %t", tt.hasRealIPheader, tt.hasIPExtractor)
+		assert.Equal(t, tt.expectedXRealIP, req.Header.Get(echo.HeaderXRealIP), "hasRealIPheader: %t / hasIPExtractor: %t", tt.hasRealIPheader, tt.hasIPExtractor)
 	}
 }
 
@@ -747,3 +747,63 @@ func TestProxyBalancerWithNoTargets(t *testing.T) {
 	rrb := NewRoundRobinBalancer([]*ProxyTarget{})
 	assert.Nil(t, rrb.Next(nil))
 }
+
+type testContextKey string
+
+type customBalancer struct {
+	target *ProxyTarget
+}
+
+func (b *customBalancer) AddTarget(target *ProxyTarget) bool {
+	return false
+}
+
+func (b *customBalancer) RemoveTarget(name string) bool {
+	return false
+}
+
+func (b *customBalancer) Next(c echo.Context) *ProxyTarget {
+	ctx := context.WithValue(c.Request().Context(), testContextKey("FROM_BALANCER"), "CUSTOM_BALANCER")
+	c.SetRequest(c.Request().WithContext(ctx))
+	return b.target
+}
+
+func TestModifyResponseUseContext(t *testing.T) {
+	server := httptest.NewServer(
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusOK)
+			w.Write([]byte("OK"))
+		}),
+	)
+	defer server.Close()
+
+	targetURL, _ := url.Parse(server.URL)
+	e := echo.New()
+	e.Use(ProxyWithConfig(
+		ProxyConfig{
+			Balancer: &customBalancer{
+				target: &ProxyTarget{
+					Name: "tst",
+					URL:  targetURL,
+				},
+			},
+			RetryCount: 1,
+			ModifyResponse: func(res *http.Response) error {
+				val := res.Request.Context().Value(testContextKey("FROM_BALANCER"))
+				if valStr, ok := val.(string); ok {
+					res.Header.Set("FROM_BALANCER", valStr)
+				}
+				return nil
+			},
+		},
+	))
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e.ServeHTTP(rec, req)
+
+	assert.Equal(t, http.StatusOK, rec.Code)
+	assert.Equal(t, "OK", rec.Body.String())
+	assert.Equal(t, "CUSTOM_BALANCER", rec.Header().Get("FROM_BALANCER"))
+}
diff --git a/middleware/rate_limiter_test.go b/middleware/rate_limiter_test.go
index 89d9a6e..f66961f 100644
--- a/middleware/rate_limiter_test.go
+++ b/middleware/rate_limiter_test.go
@@ -2,7 +2,6 @@ package middleware
 
 import (
 	"errors"
-	"fmt"
 	"math/rand"
 	"net/http"
 	"net/http/httptest"
@@ -11,7 +10,6 @@ import (
 	"time"
 
 	"github.com/labstack/echo/v4"
-	"github.com/labstack/gommon/random"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/time/rate"
 )
@@ -340,7 +338,7 @@ func TestRateLimiterMemoryStore_Allow(t *testing.T) {
 
 	for i, tc := range testCases {
 		t.Logf("Running testcase #%d => %v", i, time.Duration(i)*220*time.Millisecond)
-		now = func() time.Time {
+		inMemoryStore.timeNow = func() time.Time {
 			return time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Add(time.Duration(i) * 220 * time.Millisecond)
 		}
 		allowed, _ := inMemoryStore.Allow(tc.id)
@@ -350,24 +348,22 @@ func TestRateLimiterMemoryStore_Allow(t *testing.T) {
 
 func TestRateLimiterMemoryStore_cleanupStaleVisitors(t *testing.T) {
 	var inMemoryStore = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 1, Burst: 3})
-	now = time.Now
-	fmt.Println(now())
 	inMemoryStore.visitors = map[string]*Visitor{
 		"A": {
 			Limiter:  rate.NewLimiter(1, 3),
-			lastSeen: now(),
+			lastSeen: time.Now(),
 		},
 		"B": {
 			Limiter:  rate.NewLimiter(1, 3),
-			lastSeen: now().Add(-1 * time.Minute),
+			lastSeen: time.Now().Add(-1 * time.Minute),
 		},
 		"C": {
 			Limiter:  rate.NewLimiter(1, 3),
-			lastSeen: now().Add(-5 * time.Minute),
+			lastSeen: time.Now().Add(-5 * time.Minute),
 		},
 		"D": {
 			Limiter:  rate.NewLimiter(1, 3),
-			lastSeen: now().Add(-10 * time.Minute),
+			lastSeen: time.Now().Add(-10 * time.Minute),
 		},
 	}
 
@@ -413,7 +409,7 @@ func TestNewRateLimiterMemoryStore(t *testing.T) {
 func generateAddressList(count int) []string {
 	addrs := make([]string, count)
 	for i := 0; i < count; i++ {
-		addrs[i] = random.String(15)
+		addrs[i] = randomString(15)
 	}
 	return addrs
 }
diff --git a/middleware/util_test.go b/middleware/util_test.go
index df1d262..d0f20bb 100644
--- a/middleware/util_test.go
+++ b/middleware/util_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func Test_matchScheme(t *testing.T) {
@@ -93,3 +94,55 @@ func Test_matchSubdomain(t *testing.T) {
 		assert.Equal(t, v.expected, matchSubdomain(v.domain, v.pattern))
 	}
 }
+
+func TestRandomString(t *testing.T) {
+	var testCases = []struct {
+		name       string
+		whenLength uint8
+		expect     string
+	}{
+		{
+			name:       "ok, 16",
+			whenLength: 16,
+		},
+		{
+			name:       "ok, 32",
+			whenLength: 32,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			uid := randomString(tc.whenLength)
+			assert.Len(t, uid, int(tc.whenLength))
+		})
+	}
+}
+
+func TestRandomStringBias(t *testing.T) {
+	t.Parallel()
+	const slen = 33
+	const loop = 100000
+
+	counts := make(map[rune]int)
+	var count int64
+
+	for i := 0; i < loop; i++ {
+		s := randomString(slen)
+		require.Equal(t, slen, len(s))
+		for _, b := range s {
+			counts[b]++
+			count++
+		}
+	}
+
+	require.Equal(t, randomStringCharsetLen, len(counts))
+
+	avg := float64(count) / float64(len(counts))
+	for k, n := range counts {
+		diff := float64(n) / avg
+		if diff < 0.95 || diff > 1.05 {
+			t.Errorf("Bias on '%c': expected average %f, got %d", k, avg, n)
+		}
+	}
+}