Repository: labstack/echo
Pull request #1681 (2020-11-12 10:56:33)

separate methods to bind only: query params, route params, request body
To allow user more precise control over where bind takes its source data, bind is refactored to consist of 3 different methods that can be user separately.

1. `DefaultBinder.BindPathParams` - binds only path params to given interface
2. `DefaultBinder.BindQueryParams` - binds only query params to given interface
3. `DefaultBinder.BindBody` - binds request body to given interface

This is not breaking change as `Binder` interface was not changed. 

Example usage would be:
```go
var payload User
if err := (&DefaultBinder{}).BindBody(c, &payload); err != nil {
	return c.JSON(http.StatusBadRequest, Map{"error": err})
}
```

Also added bunch of tests for cases when bind target is filled from multiple different sources - to document how binding priority works.

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=h1) Report
> Merging [#1681](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=desc) (f2ab073) into [master](https://codecov.io/gh/labstack/echo/commit/7a90304d2daa6cc94f6d383f8aaaaaa960a61d9a?el=desc) (7a90304) will **increase** coverage by `0.42%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1681/graphs/tree.svg?width=650&height=150&src=pr&token=vqnT76l79i)](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1681      +/-   ##
==========================================
+ Coverage   84.35%   84.77%   +0.42%     
==========================================
  Files          28       28              
  Lines        1911     1918       +7     
==========================================
+ Hits         1612     1626      +14     
+ Misses        189      184       -5     
+ Partials      110      108       -2     
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [bind.go](https://codecov.io/gh/labstack/echo/pull/1681/diff?src=pr&el=tree#diff-YmluZC5nbw==) | `90.00% <100.00%> (+5.03%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=footer). Last update [7a90304...f2ab073](https://codecov.io/gh/labstack/echo/pull/1681?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

aldas: Workaround for #1670 #1638 #1356
aldas: @vishr @lammel or other Echo official maintainers - I think Echo documentation at https://echo.labstack.com/guide/request needs  clear declaration for users that `c.bind()`  uses route+query+body together and if user does not want to mix (possibly) fields from body with route/query params then `c.bind()` should be avoided.   Current implementation has even effect that for following route  `/api/entity/:id`  and URL  `/api/entity/123?id=999`  and bind target `struct{ id int }`   would end up as `999` in `id` field which is probably not at all intuitive.

to be honest. documentation does not even mention that query and route is binded along with body

> To bind request body into a Go type use Context#Bind(i interface{}). The default binder supports decoding application/json, application/xml and application/x-www-form-urlencoded data based on the Content-Type header.
> Example below binds the request payload into User struct based on tags:
iambenkay: Why isn't this merged yet?
lammel: Lets give an update on the delays in merging this PR... but it sure is a definite candidate for merging soon.

We still need to discuss the best way forward. Either use this workaround as is and put a burden on the developer or accept that we need to introduce a breaking change in the Bind interface to remedy the situation.

Feedback is welcome meanwhile.
aldas: About this PR. I should have reversed arguments for these new functions. So `BindQueryParams(i interface{}, c Context) error` would be `BindQueryParams(c Context, i interface{}) error` instead. As a public API this would make more sense.

Looking at request example https://echo.labstack.com/guide/request I think that original Echo author(s) intentions were that binding for query params should work **only** then `query:` tag exists in bind target struct. This would mean that c.Bind() behavior works mostly/only by explicitly written tags - for query params. Maybe current behavior (binding without a tag) is a bug?

I think documentation/cookbook need proper explanation how binding works.  

* How route/query/body binding can be done 
    * all in one (like current example + route param)
    * separately - sometimes you do not want to mix query with body. For json binging it could be even example one-liner with `encoding/json` but only  query params to struct Echo is currently lacking. 
    * clear explanation in which order c.Bind() binding works (route-query-body). 
    * clear warning about limitations - route/query params can not be bind to slice etc

Maybe in v4 just provide new methods to do binding separately and make sure that route/query params are bind only then their `tag` exists. And maybe in v5 consider removing `c.Bind()` and leave only separate functions as binding from multiple sources is hard to understand as very few people read documentation.

For consideration. These are examples for similar functionality in other popular frameworks

* gin - has binding (separate funcs) https://github.com/gin-gonic/gin#model-binding-and-validation
* beego - has binding https://beego.me/docs/mvc/controller/params.md#data-bind
* chi - has binding (body to struct / custom written binder) https://github.com/go-chi/render/blob/master/render.go#L20 chi documentation seems to be non-existent about binding so i'll just reference to code.


lammel: > About this PR. I should have reversed arguments for these new functions. So `BindQueryParams(i interface{}, c Context) error` would be `BindQueryParams(c Context, i interface{}) error` instead. As a public API this would make more sense.

Yes, that would be more in line with the current interfaces.

> Looking at request example https://echo.labstack.com/guide/request I think that original Echo author(s) intentions were that binding for query params should work **only** then `query:` tag exists in bind target struct. This would mean that c.Bind() behavior works mostly/only by explicitly written tags - for query params. Maybe current behavior (binding without a tag) is a bug?

We need to look into that, that may be the simplest solution.

> I think documentation/cookbook need proper explanation how binding works.

Documentation definitly needs to be updated to describe the correct behaviour then.

> Maybe in v4 just provide new methods to do binding separately and make sure that route/query params are bind only then their `tag` exists. And maybe in v5 consider removing `c.Bind()` and leave only separate functions as binding from multiple sources is hard to understand as very few people read documentation.

This is what I was thinking about.

> For consideration. These are examples for similar functionality in other popular frameworks
> 
> * gin - has binding (separate funcs) https://github.com/gin-gonic/gin#model-binding-and-validation
> * beego - has binding https://beego.me/docs/mvc/controller/params.md#data-bind
> * chi - has binding (body to struct / custom written binder) https://github.com/go-chi/render/blob/master/render.go#L20 chi documentation seems to be non-existent about binding so i'll just reference to code.

Thanks for the list, very helpful.
pafuent: https://github.com/labstack/echo/blob/502cce28d59a137f2b3ab5e907685454ee43db57/bind.go#L43-L48
I think that the error here is to assume that because bindData fails, Bind should fail. The calls for bindData are kind of optional or "call it just because this could be a _param/query_ binding". For this reason, bindData should only fail when the intention of the Echo user is to consume that kind of binding and there is an error during that binding. As an example, bindData should fail on line 46 if the type to bind is an struct, contains _query_ tags and the bind of a particular struct field fails.
To recap, bindData could be modified to not return an error if the type to bind is not an struct (the check must be kept to not panic when NumField() is called). Also, this change could be beneficial for #1356. This shouldn't be an issue for the Echo users that relies on "param/query" binding, because they are already using structs on their calls to Echo#Bind, so they could safely update to a possible newer version that doesn't fail if the type to bind isn't a struct.


aldas: I agree that sometimes it should not error out or at least let user to have choice. But this is current API behavior/implementation not a bug. It makes sense to return early when some field binding fails due data type problems. This would flush out problems **faster** for your API clients that have *incorrect* implementation. If you have worked on rewriting legacy APIs then it is absolute nightmare when percentage of clients are sending some fields as strings instead of number etc and legacy app was not strict about that (ala js/php are pretty relaxed about types).

I'll add here that order of binding -> route params before query does not make sense for me. Example `/api/user/:id/roles?id=1` would bind query param last.

so we have these questions/problems for `bind`:

* what parts of binding is optional / when to return?
* in what order should binding done?
* should only tagged fields be binded for route/query?
* should user have choice what is what is not binded (route/query/body)?
* documentation is out of date. `Bind` does **magick** - docs is missing detailed explanation how binding works, how and when to use it and what to look out.

`Bind` does too much and in unclear way. Is there a reason why must bind do 3 things (binding route/query/body) in one call?



iambenkay: Tiny suggestion that can be added to the PR. Usually when working with echo I have to manually process my comma separated query values. Would be great to abstract that into the `bindQueryParams` method.

basically
`/sort?by=name,email` should resolve the `by` param to a slice `[name, email]`

any thoughts?
pafuent: > I agree that sometimes it should not error out or at least let user to have choice. But this is current API behavior/implementation not a bug. It makes sense to return early when some field binding fails due data type problems. This would flush out problems faster for your API clients that have incorrect implementation. If you have worked on rewriting legacy APIs then it is absolute nightmare when percentage of clients are sending some fields as strings instead of number etc and legacy app was not strict about that (ala js/php are pretty relaxed about types).

Sorry, it seems that I didn't express my idea properly. The only error that could be removed is the struct type check. The rest of the errors should be kept for the reasons that you are mentioning. For me, that case is not an error because calling bindData() on line 43 and 46 it's a "try to guess if the variable to bind is a struct properly tagged". The only caveat to relaxing this error (not a struct) could be that the binded variable will remain as it was before the call to bindData, but probably that couldn't be a big problem because before its use some kind of validation must be performed on it.

Here are mi opinions about some of your questions:

>what parts of binding is optional / when to return?

The answer to this one is kind of tricky, because to know 100% sure if the calls to bindData for param/query are not optional, the type must be validated and then all the fields needs to be check for the presence of the corresponding tag, and if you find that all this is true, you will do it again the same inside bindData.

>should only tagged fields be binded for route/query?

I think yes. The autobinding by name could be nice for some users, but in my case I always prefer being explicit. Also I think it could simplify the code, because the intention of the user will be always be clear.

>should user have choice what is what is not binded (route/query/body)?

Yes, and could be implemented with the three functions that you are proposing: BindRouteParams/BindQueryParams/BindBody



pafuent: @iambenkay I'm don't have a strong position on your suggestion, but maybe what we can discuss if it's worthy to implement ourselves in Echo a more complex DefaultBinder or if we should use a third party Binder as DefaultBinder. Of course, the current implementation of Echo have the mechanisms needed to select/use the Binder that better suit your needs, but it's kind of common that Echo users fills issues because the DefaultBinder doesn't work as they assume/need.
iambenkay: @pafuent I think the current echo default binder is just fine. Just  so far it has delivered well. Just a few hiccups like binding all the data units in a request at once and the comma separated query string for me. This PR obliterates the first issue.

maybe when this is merged, I could send in a PR that builds on this to implement the comma separated query values.

I understand that there is always the option of using a different binder altogether but if most users are like me, then they don't really like replacing `Defaults` in frameworks seeing that they are perfectly suited to the framework. Would be great if we could add this feature.
aldas: Offtopic: this should be discussed in separate issue

just finished porting older php app to go and there is one nice feature that PHP has - mapping `&param[]=1&param[]=2` to array `[1,2]`.  And looking at Gin API there are methods to bind params to native type ala  Int64, Float64, Bool - these are also nice things that I always have as similar utility functions in my Echo apps.

ala 

```go
func ToValidInt64(input string, defaultValue int64) (int64, bool) {
	if input == "" {
		return defaultValue, true
	}
	n, err := strconv.Atoi(input)
	if err != nil {
		return 0, false
	}
	return int64(n), true
}
```

and in handlers
```go
	length, ok := sanitizer.ToValidInt64(c.QueryParam("length"), 25)
	if !ok {
		return apperror.NewValidationError("length is not valid int64 value")
	}
```

Maybe echo could have special func/structs for handling things like that. ala very short API for usage like that. 

```go
length := int64(25) // default value
var csv bool
var params []string

b := echo.BindQuery(c) // deals only with query params
if err := b.Int64(&length, "length"); err != nil {
    return errors.wrap(err, "length is not valid int64 value")
}
if err := b.Bool(&csv, "csv"); err != nil {
    return errors.wrap(err, "csv is not valid bool value")
}
if err := b.SliceStrings(&params, "params"); err != nil {
    return errors.wrap(err, "params contains invalid values")
}
```

or even this - fluent APIs are not *very* idiomatic go but for me I usually have bunch of query params that I need to bind and need to check for error (first) and return some meaningful message
```go 
 // deals only with query params, returns on first error, error struct contains field name as separate member
err := echo.BindQuery(c).
   Int64(&length, "length").
   Bool(&csv, "csv").
   Bind() // and ala BindErrs() that tries to bind all returns all errors
if err != nil {
   return errors.Wrapf(err, "%s can not be bind", (err.(echo.BindError)).field)
}
```

I have not used any library for this kind of stuff but that later one is probably already implemented in some binding/validation or something-something library
lammel: After some discussion with @pafuent on the current situation we think the best way forward is a follows:

1. The current DefaultBinder must not consider route or query params unless explicitely specified for the struct (this is what is expected by most users and matches the earlier behaviour. We know that this can be considered breaking, but actually it will restore previour behaviour)
2. Tests will need to be added to assure that binding will not occur for query and route params unless explicitely tagged in the struct
3. The documentation will be corrected to state this change in behaviour
4. An additonal binder `AutoBinder` (next to DefaultBinder in the core) that will perform binding against all sources (including query and route params) like in the current implementation and add it to the documentation (this should help users relying those that rely on the current behaviour
5. The behaviour of bind failing not non-structs needs to be reviewed and tests with a feasable usecase added (e.g. binding to an array of structs or a map)
6. The current changes using separate functions for BindQueryParam, BindRouteParam, BindBodyParam can be kept, but should get some love in the documentation then.

The changes above will be done for v4 now. Future changes for binding will be discussed in a seperate issue/PR for v5 (where also the interface can change)

We could use this PR to work towards the wanted behaviour above for 1) and 2). 
Documentation updates for 3) and 4) should be in seperate PRs in the labstack/echox projekt.
New PR for the autobinder and non-struct binding will need to be opened, I'm not yet sure if 4) the AutoBinder  with the current behaviour is really needed.

@aldas: If you are willing to contribute we can keep this PR and adjust to the behaiour and tests.
We can use your newly opened PR labstack/echox#169 for the documentation part.

Feedback very welcome!

Patch:
diff --git a/.github/stale.yml b/.github/stale.yml
index 04dd169..d9f6563 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -1,19 +1,17 @@
 # Number of days of inactivity before an issue becomes stale
 daysUntilStale: 60
 # Number of days of inactivity before a stale issue is closed
-daysUntilClose: 30
+daysUntilClose: 7
 # Issues with these labels will never be considered stale
 exemptLabels:
   - pinned
   - security
-  - bug
-  - enhancement
 # Label to use when marking an issue as stale
-staleLabel: stale
+staleLabel: wontfix
 # Comment to post when marking an issue as stale. Set to `false` to disable
 markComment: >
   This issue has been automatically marked as stale because it has not had
-  recent activity. It will be closed within a month if no further activity occurs.
-  Thank you for your contributions.
+  recent activity. It will be closed if no further activity occurs. Thank you
+  for your contributions.
 # Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
+closeComment: false
\ No newline at end of file
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index 2aec272..38596ab 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -9,7 +9,6 @@ on:
       - 'go.*'
       - '_fixture/**'
       - '.github/**'
-      - 'codecov.yml'
   pull_request:
     branches:
       - master
@@ -18,14 +17,17 @@ on:
       - 'go.*'
       - '_fixture/**'
       - '.github/**'
-      - 'codecov.yml'
+
+env:
+  GO111MODULE: on
+  GOPROXY: https://proxy.golang.org
 
 jobs:
   test:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        go: [1.12, 1.13, 1.14, 1.15]
+        go: [1.12, 1.13, 1.14]
     name: ${{ matrix.os }} @ Go ${{ matrix.go }}
     runs-on: ${{ matrix.os }}
     steps:
@@ -36,15 +38,10 @@ jobs:
 
       - name: Set GOPATH and PATH
         run: |
-          echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
-          echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
+          echo "::set-env name=GOPATH::$(dirname $GITHUB_WORKSPACE)"
+          echo "::add-path::$(dirname $GITHUB_WORKSPACE)/bin"
         shell: bash
 
-      - name: Set build variables
-        run: |
-          echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
-          echo "GO111MODULE=on" >> $GITHUB_ENV
-
       - name: Checkout Code
         uses: actions/checkout@v1
         with:
@@ -64,55 +61,3 @@ jobs:
         with:
           token:
           fail_ci_if_error: false
-  benchmark:
-    needs: test
-    strategy:
-      matrix:
-        os: [ubuntu-latest]
-        go: [1.15]
-    name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
-    runs-on: ${{ matrix.os }}
-    steps:
-      - name: Set up Go ${{ matrix.go }}
-        uses: actions/setup-go@v1
-        with:
-          go-version: ${{ matrix.go }}
-
-      - name: Set GOPATH and PATH
-        run: |
-          echo "GOPATH=$(dirname $GITHUB_WORKSPACE)" >> $GITHUB_ENV
-          echo "$(dirname $GITHUB_WORKSPACE)/bin" >> $GITHUB_PATH
-        shell: bash
-
-      - name: Set build variables
-        run: |
-          echo "GOPROXY=https://proxy.golang.org" >> $GITHUB_ENV
-          echo "GO111MODULE=on" >> $GITHUB_ENV
-
-      - name: Checkout Code (Previous)
-        uses: actions/checkout@v2
-        with:
-          ref: ${{ github.base_ref }}
-          path: previous
-
-      - name: Checkout Code (New)
-        uses: actions/checkout@v2
-        with:
-          path: new
-
-      - name: Install Dependencies
-        run: go get -v golang.org/x/perf/cmd/benchstat
-
-      - name: Run Benchmark (Previous)
-        run: |
-          cd previous
-          go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt
-
-      - name: Run Benchmark (New)
-        run: |
-          cd new
-          go test -run="-" -bench=".*" -count=8 ./... > benchmark.txt
-
-      - name: Run Benchstat
-        run: |
-          benchstat previous/benchmark.txt new/benchmark.txt
diff --git a/.travis.yml b/.travis.yml
index 67d45ad..ef826e9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,3 @@
-arch:
-  - amd64
-  - ppc64le
-  
 language: go
 go:
   - 1.14.x
diff --git a/README.md b/README.md
index deba54f..03ad4dc 100644
--- a/README.md
+++ b/README.md
@@ -42,14 +42,11 @@ For older versions, please use the latest v3 tag.
 
 ## Benchmarks
 
-Date: 2020/11/11<br>
+Date: 2018/03/15<br>
 Source: https://github.com/vishr/web-framework-benchmark<br>
 Lower is better!
 
-<img src="https://i.imgur.com/qwPNQbl.png">
-<img src="https://i.imgur.com/s8yKQjx.png">
-
-The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
+<img src="https://i.imgur.com/I32VdMJ.png">
 
 ## [Guide](https://echo.labstack.com/guide)
 
diff --git a/_fixture/_fixture/README.md b/_fixture/_fixture/README.md
deleted file mode 100644
index 21a7858..0000000
--- a/_fixture/_fixture/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-This directory is used for the static middleware test
\ No newline at end of file
diff --git a/bind.go b/bind.go
index f891474..c7be242 100644
--- a/bind.go
+++ b/bind.go
@@ -30,10 +30,8 @@ type (
 	}
 )
 
-// Bind implements the `Binder#Bind` function.
-func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
-	req := c.Request()
-
+// BindPathParams binds path params to bindable object
+func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
 	names := c.ParamNames()
 	values := c.ParamValues()
 	params := map[string][]string{}
@@ -43,12 +41,28 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
 	if err := b.bindData(i, params, "param"); err != nil {
 		return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
 	}
-	if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
+	return nil
+}
+
+// BindQueryParams binds query params to bindable object
+func (b *DefaultBinder) BindQueryParams(c Context, i interface{}) error {
+	if err := b.bindData(i, c.QueryParams(), "query"); err != nil {
 		return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
 	}
+	return nil
+}
+
+// BindBody binds request body contents to bindable object
+// NB: then binding forms take note that this implementation uses standard library form parsing
+// which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm
+// See non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm
+// See MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseMultipartForm
+func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
+	req := c.Request()
 	if req.ContentLength == 0 {
 		return
 	}
+
 	ctype := req.Header.Get(HeaderContentType)
 	switch {
 	case strings.HasPrefix(ctype, MIMEApplicationJSON):
@@ -80,7 +94,18 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
 	default:
 		return ErrUnsupportedMediaType
 	}
-	return
+	return nil
+}
+
+// Bind implements the `Binder#Bind` function.
+func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
+	if err := b.BindPathParams(c, i); err != nil {
+		return err
+	}
+	if err = b.BindQueryParams(c, i); err != nil {
+		return err
+	}
+	return b.BindBody(c, i)
 }
 
 func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 0fa3a3f..0000000
--- a/codecov.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-coverage:
-  status:
-    project:
-      default:
-        threshold: 1%
-    patch:
-      default:
-        threshold: 1%
-
-comment:
-  require_changes: true
\ No newline at end of file
diff --git a/context.go b/context.go
index 8ba9847..0507f13 100644
--- a/context.go
+++ b/context.go
@@ -365,7 +365,7 @@ func (c *context) FormFile(name string) (*multipart.FileHeader, error) {
 	if err != nil {
 		return nil, err
 	}
-	f.Close()
+	defer f.Close()
 	return fh, nil
 }
 
diff --git a/echo.go b/echo.go
index 3816041..29b88b7 100644
--- a/echo.go
+++ b/echo.go
@@ -92,7 +92,6 @@ type (
 		Renderer         Renderer
 		Logger           Logger
 		IPExtractor      IPExtractor
-		ListenerNetwork  string
 	}
 
 	// Route contains a handler and information for matching against requests.
@@ -282,7 +281,6 @@ var (
 	ErrInvalidRedirectCode         = errors.New("invalid redirect status code")
 	ErrCookieNotFound              = errors.New("cookie not found")
 	ErrInvalidCertOrKeyType        = errors.New("invalid cert or key type, must be string or []byte")
-	ErrInvalidListenerNetwork      = errors.New("invalid listener network")
 )
 
 // Error handlers
@@ -304,10 +302,9 @@ func New() (e *Echo) {
 		AutoTLSManager: autocert.Manager{
 			Prompt: autocert.AcceptTOS,
 		},
-		Logger:          log.New("echo"),
-		colorer:         color.New(),
-		maxParam:        new(int),
-		ListenerNetwork: "tcp",
+		Logger:   log.New("echo"),
+		colorer:  color.New(),
+		maxParam: new(int),
 	}
 	e.Server.Handler = e
 	e.TLSServer.Handler = e
@@ -575,7 +572,7 @@ func (e *Echo) Reverse(name string, params ...interface{}) string {
 	for _, r := range e.router.routes {
 		if r.Name == name {
 			for i, l := 0, len(r.Path); i < l; i++ {
-				if (r.Path[i] == ':' || r.Path[i] == '*') && n < ln {
+				if r.Path[i] == ':' && n < ln {
 					for ; i < l && r.Path[i] != '/'; i++ {
 					}
 					uri.WriteString(fmt.Sprintf("%v", params[n]))
@@ -717,7 +714,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
 
 	if s.TLSConfig == nil {
 		if e.Listener == nil {
-			e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
+			e.Listener, err = newListener(s.Addr)
 			if err != nil {
 				return err
 			}
@@ -728,7 +725,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
 		return s.Serve(e.Listener)
 	}
 	if e.TLSListener == nil {
-		l, err := newListener(s.Addr, e.ListenerNetwork)
+		l, err := newListener(s.Addr)
 		if err != nil {
 			return err
 		}
@@ -757,7 +754,7 @@ func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
 	}
 
 	if e.Listener == nil {
-		e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
+		e.Listener, err = newListener(s.Addr)
 		if err != nil {
 			return err
 		}
@@ -878,11 +875,8 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
 	return
 }
 
-func newListener(address, network string) (*tcpKeepAliveListener, error) {
-	if network != "tcp" && network != "tcp4" && network != "tcp6" {
-		return nil, ErrInvalidListenerNetwork
-	}
-	l, err := net.Listen(network, address)
+func newListener(address string) (*tcpKeepAliveListener, error) {
+	l, err := net.Listen("tcp", address)
 	if err != nil {
 		return nil, err
 	}
diff --git a/middleware/compress.go b/middleware/compress.go
index 6ae1974..e4f9fc5 100644
--- a/middleware/compress.go
+++ b/middleware/compress.go
@@ -59,7 +59,7 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
 		config.Level = DefaultGzipConfig.Level
 	}
 
-	pool := gzipCompressPool(config)
+	pool := gzipPool(config)
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
 		return func(c echo.Context) error {
@@ -133,7 +133,7 @@ func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
 	return http.ErrNotSupported
 }
 
-func gzipCompressPool(config GzipConfig) sync.Pool {
+func gzipPool(config GzipConfig) sync.Pool {
 	return sync.Pool{
 		New: func() interface{} {
 			w, err := gzip.NewWriterLevel(ioutil.Discard, config.Level)
diff --git a/middleware/cors.go b/middleware/cors.go
index d6ef896..c263f73 100644
--- a/middleware/cors.go
+++ b/middleware/cors.go
@@ -19,13 +19,6 @@ type (
 		// Optional. Default value []string{"*"}.
 		AllowOrigins []string `yaml:"allow_origins"`
 
-		// AllowOriginFunc is a custom function to validate the origin. It takes the
-		// origin as an argument and returns true if allowed or false otherwise. If
-		// an error is returned, it is returned by the handler. If this option is
-		// set, AllowOrigins is ignored.
-		// Optional.
-		AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"`
-
 		// AllowMethods defines a list methods allowed when accessing the resource.
 		// This is used in response to a preflight request.
 		// Optional. Default value DefaultCORSConfig.AllowMethods.
@@ -109,73 +102,45 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 			origin := req.Header.Get(echo.HeaderOrigin)
 			allowOrigin := ""
 
-			preflight := req.Method == http.MethodOptions
-			res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
-
-			// No Origin provided
-			if origin == "" {
-				if !preflight {
-					return next(c)
+			// Check allowed origins
+			for _, o := range config.AllowOrigins {
+				if o == "*" && config.AllowCredentials {
+					allowOrigin = origin
+					break
 				}
-				return c.NoContent(http.StatusNoContent)
-			}
-
-			if config.AllowOriginFunc != nil {
-				allowed, err := config.AllowOriginFunc(origin)
-				if err != nil {
-					return err
+				if o == "*" || o == origin {
+					allowOrigin = o
+					break
 				}
-				if allowed {
+				if matchSubdomain(origin, o) {
 					allowOrigin = origin
+					break
 				}
-			} else {
-				// Check allowed origins
-				for _, o := range config.AllowOrigins {
-					if o == "*" && config.AllowCredentials {
-						allowOrigin = origin
-						break
+			}
+
+			// Check allowed origin patterns
+			for _, re := range allowOriginPatterns {
+				if allowOrigin == "" {
+					didx := strings.Index(origin, "://")
+					if didx == -1 {
+						continue
 					}
-					if o == "*" || o == origin {
-						allowOrigin = o
+					domAuth := origin[didx+3:]
+					// to avoid regex cost by invalid long domain
+					if len(domAuth) > 253 {
 						break
 					}
-					if matchSubdomain(origin, o) {
+
+					if match, _ := regexp.MatchString(re, origin); match {
 						allowOrigin = origin
 						break
 					}
 				}
-
-				// Check allowed origin patterns
-				for _, re := range allowOriginPatterns {
-					if allowOrigin == "" {
-						didx := strings.Index(origin, "://")
-						if didx == -1 {
-							continue
-						}
-						domAuth := origin[didx+3:]
-						// to avoid regex cost by invalid long domain
-						if len(domAuth) > 253 {
-							break
-						}
-
-						if match, _ := regexp.MatchString(re, origin); match {
-							allowOrigin = origin
-							break
-						}
-					}
-				}
-			}
-
-			// Origin not allowed
-			if allowOrigin == "" {
-				if !preflight {
-					return next(c)
-				}
-				return c.NoContent(http.StatusNoContent)
 			}
 
 			// Simple request
-			if !preflight {
+			if req.Method != http.MethodOptions {
+				res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
 				res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
 				if config.AllowCredentials {
 					res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
@@ -187,6 +152,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
 			}
 
 			// Preflight request
+			res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
 			res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod)
 			res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders)
 			res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigin)
diff --git a/middleware/decompress.go b/middleware/decompress.go
deleted file mode 100644
index c046359..0000000
--- a/middleware/decompress.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package middleware
-
-import (
-	"bytes"
-	"compress/gzip"
-	"io"
-	"io/ioutil"
-	"net/http"
-	"sync"
-
-	"github.com/labstack/echo/v4"
-)
-
-type (
-	// DecompressConfig defines the config for Decompress middleware.
-	DecompressConfig struct {
-		// Skipper defines a function to skip middleware.
-		Skipper Skipper
-
-		// GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers
-		GzipDecompressPool Decompressor
-	}
-)
-
-//GZIPEncoding content-encoding header if set to "gzip", decompress body contents.
-const GZIPEncoding string = "gzip"
-
-// Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers
-type Decompressor interface {
-	gzipDecompressPool() sync.Pool
-}
-
-var (
-	//DefaultDecompressConfig defines the config for decompress middleware
-	DefaultDecompressConfig = DecompressConfig{
-		Skipper:            DefaultSkipper,
-		GzipDecompressPool: &DefaultGzipDecompressPool{},
-	}
-)
-
-// DefaultGzipDecompressPool is the default implementation of Decompressor interface
-type DefaultGzipDecompressPool struct {
-}
-
-func (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool {
-	return sync.Pool{
-		New: func() interface{} {
-			// create with an empty reader (but with GZIP header)
-			w, err := gzip.NewWriterLevel(ioutil.Discard, gzip.BestSpeed)
-			if err != nil {
-				return err
-			}
-
-			b := new(bytes.Buffer)
-			w.Reset(b)
-			w.Flush()
-			w.Close()
-
-			r, err := gzip.NewReader(bytes.NewReader(b.Bytes()))
-			if err != nil {
-				return err
-			}
-			return r
-		},
-	}
-}
-
-//Decompress decompresses request body based if content encoding type is set to "gzip" with default config
-func Decompress() echo.MiddlewareFunc {
-	return DecompressWithConfig(DefaultDecompressConfig)
-}
-
-//DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config
-func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {
-	// Defaults
-	if config.Skipper == nil {
-		config.Skipper = DefaultGzipConfig.Skipper
-	}
-	if config.GzipDecompressPool == nil {
-		config.GzipDecompressPool = DefaultDecompressConfig.GzipDecompressPool
-	}
-
-	return func(next echo.HandlerFunc) echo.HandlerFunc {
-		pool := config.GzipDecompressPool.gzipDecompressPool()
-		return func(c echo.Context) error {
-			if config.Skipper(c) {
-				return next(c)
-			}
-			switch c.Request().Header.Get(echo.HeaderContentEncoding) {
-			case GZIPEncoding:
-				b := c.Request().Body
-
-				i := pool.Get()
-				gr, ok := i.(*gzip.Reader)
-				if !ok {
-					return echo.NewHTTPError(http.StatusInternalServerError, i.(error).Error())
-				}
-
-				if err := gr.Reset(b); err != nil {
-					pool.Put(gr)
-					if err == io.EOF { //ignore if body is empty
-						return next(c)
-					}
-					return err
-				}
-				var buf bytes.Buffer
-				io.Copy(&buf, gr)
-
-				gr.Close()
-				pool.Put(gr)
-
-				b.Close() // http.Request.Body is closed by the Server, but because we are replacing it, it must be closed here
-
-				r := ioutil.NopCloser(&buf)
-				c.Request().Body = r
-			}
-			return next(c)
-		}
-	}
-}
diff --git a/middleware/jwt.go b/middleware/jwt.go
index da00ea5..bab00c9 100644
--- a/middleware/jwt.go
+++ b/middleware/jwt.go
@@ -57,7 +57,6 @@ type (
 		// - "query:<name>"
 		// - "param:<name>"
 		// - "cookie:<name>"
-		// - "form:<name>"
 		TokenLookup string
 
 		// AuthScheme to be used in the Authorization header.
@@ -168,8 +167,6 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
 		extractor = jwtFromParam(parts[1])
 	case "cookie":
 		extractor = jwtFromCookie(parts[1])
-	case "form":
-		extractor = jwtFromForm(parts[1])
 	}
 
 	return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -269,14 +266,3 @@ func jwtFromCookie(name string) jwtExtractor {
 		return cookie.Value, nil
 	}
 }
-
-// jwtFromForm returns a `jwtExtractor` that extracts token from the form field.
-func jwtFromForm(name string) jwtExtractor {
-	return func(c echo.Context) (string, error) {
-		field := c.FormValue(name)
-		if field == "" {
-			return "", ErrJWTMissing
-		}
-		return field, nil
-	}
-}
diff --git a/middleware/static.go b/middleware/static.go
index 58b7890..bc2087a 100644
--- a/middleware/static.go
+++ b/middleware/static.go
@@ -36,12 +36,6 @@ type (
 		// Enable directory browsing.
 		// Optional. Default value false.
 		Browse bool `yaml:"browse"`
-
-		// Enable ignoring of the base of the URL path.
-		// Example: when assigning a static middleware to a non root path group,
-		// the filesystem path is not doubled
-		// Optional. Default value false.
-		IgnoreBase bool `yaml:"ignoreBase"`
 	}
 )
 
@@ -169,15 +163,6 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
 			}
 			name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
 
-			if config.IgnoreBase {
-				routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
-				baseURLPath := path.Base(p)
-				if baseURLPath == routePath {
-					i := strings.LastIndex(name, routePath)
-					name = name[:i] + strings.Replace(name[i:], routePath, "", 1)
-				}
-			}
-
 			fi, err := os.Stat(name)
 			if err != nil {
 				if os.IsNotExist(err) {
diff --git a/response.go b/response.go
index 84f7c9e..ca7405c 100644
--- a/response.go
+++ b/response.go
@@ -56,11 +56,11 @@ func (r *Response) WriteHeader(code int) {
 		r.echo.Logger.Warn("response already committed")
 		return
 	}
-	r.Status = code
 	for _, fn := range r.beforeFuncs {
 		fn()
 	}
-	r.Writer.WriteHeader(r.Status)
+	r.Status = code
+	r.Writer.WriteHeader(code)
 	r.Committed = true
 }
 
diff --git a/router.go b/router.go
index 4c3898c..ed728d6 100644
--- a/router.go
+++ b/router.go
@@ -428,9 +428,7 @@ func (r *Router) Find(method, path string, c Context) {
 				pos := strings.IndexByte(ns, '/')
 				if pos == -1 {
 					// If no slash is remaining in search string set param value
-					if len(cn.pnames) > 0 {
-						pvalues[len(cn.pnames)-1] = search
-					}
+					pvalues[len(cn.pnames)-1] = search
 					break
 				} else if pos > 0 {
 					// Otherwise continue route processing with restored next node


Test patch:
diff --git a/bind_test.go b/bind_test.go
index b9fb9de..60c2f9e 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -553,3 +553,305 @@ func testBindError(assert *assert.Assertions, r io.Reader, ctype string, expecte
 		}
 	}
 }
+
+func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
+	// tests to check binding behaviour when multiple sources path params, query params and request body are in use
+	// binding is done in steps and one source could overwrite previous source binded data
+	// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
+
+	type Node struct {
+		ID   int    `json:"id"`
+		Node string `json:"node"`
+	}
+
+	var testCases = []struct {
+		name             string
+		givenURL         string
+		givenContent     io.Reader
+		givenMethod      string
+		whenBindTarget   interface{}
+		whenNoPathParams bool
+		expect           interface{}
+		expectError      string
+	}{
+		{
+			name:         "ok, POST bind to struct with: path param + query param + empty body",
+			givenMethod:  http.MethodPost,
+			givenURL:     "/api/real_node/endpoint?node=xxx",
+			givenContent: strings.NewReader(`{"id": 1}`),
+			expect:       &Node{ID: 1, Node: "xxx"}, // in current implementation query params has higher priority than path params
+		},
+		{
+			name:         "ok, POST bind to struct with: path param + empty body",
+			givenMethod:  http.MethodPost,
+			givenURL:     "/api/real_node/endpoint",
+			givenContent: strings.NewReader(`{"id": 1}`),
+			expect:       &Node{ID: 1, Node: "real_node"},
+		},
+		{
+			name:         "ok, POST bind to struct with path + query + body = body has priority",
+			givenMethod:  http.MethodPost,
+			givenURL:     "/api/real_node/endpoint?node=xxx",
+			givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
+			expect:       &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
+		},
+		{
+			name:         "nok, POST body bind failure",
+			givenMethod:  http.MethodPost,
+			givenURL:     "/api/real_node/endpoint?node=xxx",
+			givenContent: strings.NewReader(`{`),
+			expect:       &Node{ID: 0, Node: "xxx"}, // query binding has already modified bind target
+			expectError:  "code=400, message=unexpected EOF, internal=unexpected EOF",
+		},
+		{
+			name:         "nok, GET body bind failure - trying to bind json array to struct",
+			givenMethod:  http.MethodGet,
+			givenURL:     "/api/real_node/endpoint?node=xxx",
+			givenContent: strings.NewReader(`[{"id": 1}]`),
+			expect:       &Node{ID: 0, Node: "xxx"}, // query binding has already modified bind target
+			expectError:  "code=400, message=Unmarshal type error: expected=echo.Node, got=array, field=, offset=1, internal=json: cannot unmarshal array into Go value of type echo.Node",
+		},
+		{ // binding query params interferes with body. b.BindBody() should be used to bind only body to slice
+			name:             "nok, GET query params bind failure - trying to bind json array to slice",
+			givenMethod:      http.MethodGet,
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenContent:     strings.NewReader(`[{"id": 1}]`),
+			whenNoPathParams: true,
+			whenBindTarget:   &[]Node{},
+			expect:           &[]Node{},
+			expectError:      "code=400, message=binding element must be a struct, internal=binding element must be a struct",
+		},
+		{ // binding path params interferes with body. b.BindBody() should be used to bind only body to slice
+			name:           "nok, GET path params bind failure - trying to bind json array to slice",
+			givenMethod:    http.MethodGet,
+			givenURL:       "/api/real_node/endpoint?node=xxx",
+			givenContent:   strings.NewReader(`[{"id": 1}]`),
+			whenBindTarget: &[]Node{},
+			expect:         &[]Node{},
+			expectError:    "code=400, message=binding element must be a struct, internal=binding element must be a struct",
+		},
+		{
+			name:             "ok, GET body bind json array to slice",
+			givenMethod:      http.MethodGet,
+			givenURL:         "/api/real_node/endpoint",
+			givenContent:     strings.NewReader(`[{"id": 1}]`),
+			whenNoPathParams: true,
+			whenBindTarget:   &[]Node{},
+			expect:           &[]Node{{ID: 1, Node: ""}},
+			expectError:      "",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := New()
+			// assume route we are testing is "/api/:node/endpoint?some_query_params=here"
+			req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)
+			req.Header.Set(HeaderContentType, MIMEApplicationJSON)
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
+
+			if !tc.whenNoPathParams {
+				c.SetParamNames("node")
+				c.SetParamValues("real_node")
+			}
+
+			var bindTarget interface{}
+			if tc.whenBindTarget != nil {
+				bindTarget = tc.whenBindTarget
+			} else {
+				bindTarget = &Node{}
+			}
+			b := new(DefaultBinder)
+
+			err := b.Bind(bindTarget, c)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tc.expect, bindTarget)
+		})
+	}
+}
+
+func TestDefaultBinder_BindBody(t *testing.T) {
+	// tests to check binding behaviour when multiple sources path params, query params and request body are in use
+	// generally when binding from request body - URL and path params are ignored - unless form is being binded.
+	// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
+
+	type Node struct {
+		ID   int    `json:"id" xml:"id"`
+		Node string `json:"node" xml:"node"`
+	}
+	type Nodes struct {
+		Nodes []Node `xml:"node" form:"node"`
+	}
+
+	var testCases = []struct {
+		name             string
+		givenURL         string
+		givenContent     io.Reader
+		givenMethod      string
+		givenContentType string
+		whenNoPathParams bool
+		whenBindTarget   interface{}
+		expect           interface{}
+		expectError      string
+	}{
+		{
+			name:             "ok, JSON POST bind to struct with: path + query + empty field in body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationJSON,
+			givenContent:     strings.NewReader(`{"id": 1}`),
+			expect:           &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body
+		},
+		{
+			name:             "ok, JSON POST bind to struct with: path + query + body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationJSON,
+			givenContent:     strings.NewReader(`{"id": 1, "node": "zzz"}`),
+			expect:           &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
+		},
+		{
+			name:             "ok, JSON POST body bind json array to slice (has matching path/query params)",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationJSON,
+			givenContent:     strings.NewReader(`[{"id": 1}]`),
+			whenNoPathParams: true,
+			whenBindTarget:   &[]Node{},
+			expect:           &[]Node{{ID: 1, Node: ""}},
+			expectError:      "",
+		},
+		{ // rare case as GET is not usually used to send request body
+			name:             "ok, JSON GET bind to struct with: path + query + empty field in body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodGet,
+			givenContentType: MIMEApplicationJSON,
+			givenContent:     strings.NewReader(`{"id": 1}`),
+			expect:           &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body
+		},
+		{ // rare case as GET is not usually used to send request body
+			name:             "ok, JSON GET bind to struct with: path + query + body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodGet,
+			givenContentType: MIMEApplicationJSON,
+			givenContent:     strings.NewReader(`{"id": 1, "node": "zzz"}`),
+			expect:           &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
+		},
+		{
+			name:             "nok, JSON POST body bind failure",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationJSON,
+			givenContent:     strings.NewReader(`{`),
+			expect:           &Node{ID: 0, Node: ""},
+			expectError:      "code=400, message=unexpected EOF, internal=unexpected EOF",
+		},
+		{
+			name:             "ok, XML POST bind to struct with: path + query + empty body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationXML,
+			givenContent:     strings.NewReader(`<node><id>1</id><node>yyy</node></node>`),
+			expect:           &Node{ID: 1, Node: "yyy"},
+		},
+		{
+			name:             "ok, XML POST bind array to slice with: path + query + body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationXML,
+			givenContent:     strings.NewReader(`<nodes><node><id>1</id><node>yyy</node></node></nodes>`),
+			whenBindTarget:   &Nodes{},
+			expect:           &Nodes{Nodes: []Node{{ID: 1, Node: "yyy"}}},
+		},
+		{
+			name:             "nok, XML POST bind failure",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationXML,
+			givenContent:     strings.NewReader(`<node><`),
+			expect:           &Node{ID: 0, Node: ""},
+			expectError:      "code=400, message=Syntax error: line=1, error=XML syntax error on line 1: unexpected EOF, internal=XML syntax error on line 1: unexpected EOF",
+		},
+		{
+			name:             "ok, FORM POST bind to struct with: path + query + empty body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationForm,
+			givenContent:     strings.NewReader(`id=1&node=yyy`),
+			expect:           &Node{ID: 1, Node: "yyy"},
+		},
+		{
+			// NB: form values are taken from BOTH body and query for POST/PUT/PATCH by standard library implementation
+			// See: https://golang.org/pkg/net/http/#Request.ParseForm
+			name:             "ok, FORM POST bind to struct with: path + query + empty field in body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMEApplicationForm,
+			givenContent:     strings.NewReader(`id=1`),
+			expect:           &Node{ID: 1, Node: "xxx"},
+		},
+		{
+			// NB: form values are taken from query by standard library implementation
+			// See: https://golang.org/pkg/net/http/#Request.ParseForm
+			name:             "ok, FORM GET bind to struct with: path + query + empty field in body",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodGet,
+			givenContentType: MIMEApplicationForm,
+			givenContent:     strings.NewReader(`id=1`),
+			expect:           &Node{ID: 0, Node: "xxx"}, // 'xxx' is taken from URL and body is not used with GET by implementation
+		},
+		{
+			name:             "nok, unsupported content type",
+			givenURL:         "/api/real_node/endpoint?node=xxx",
+			givenMethod:      http.MethodPost,
+			givenContentType: MIMETextPlain,
+			givenContent:     strings.NewReader(`<html></html>`),
+			expect:           &Node{ID: 0, Node: ""},
+			expectError:      "code=415, message=Unsupported Media Type",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := New()
+			// assume route we are testing is "/api/:node/endpoint?some_query_params=here"
+			req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)
+			switch tc.givenContentType {
+			case MIMEApplicationXML:
+				req.Header.Set(HeaderContentType, MIMEApplicationXML)
+			case MIMEApplicationForm:
+				req.Header.Set(HeaderContentType, MIMEApplicationForm)
+			case MIMEApplicationJSON:
+				req.Header.Set(HeaderContentType, MIMEApplicationJSON)
+			}
+			rec := httptest.NewRecorder()
+			c := e.NewContext(req, rec)
+
+			if !tc.whenNoPathParams {
+				c.SetParamNames("node")
+				c.SetParamValues("real_node")
+			}
+
+			var bindTarget interface{}
+			if tc.whenBindTarget != nil {
+				bindTarget = tc.whenBindTarget
+			} else {
+				bindTarget = &Node{}
+			}
+			b := new(DefaultBinder)
+
+			err := b.BindBody(c, bindTarget)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tc.expect, bindTarget)
+		})
+	}
+}
diff --git a/echo_test.go b/echo_test.go
index bf3ecfe..0368dbd 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	stdContext "context"
 	"errors"
-	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -278,12 +277,10 @@ func TestEchoURL(t *testing.T) {
 	e := New()
 	static := func(Context) error { return nil }
 	getUser := func(Context) error { return nil }
-	getAny := func(Context) error { return nil }
 	getFile := func(Context) error { return nil }
 
 	e.GET("/static/file", static)
 	e.GET("/users/:id", getUser)
-	e.GET("/documents/*", getAny)
 	g := e.Group("/group")
 	g.GET("/users/:uid/files/:fid", getFile)
 
@@ -292,9 +289,6 @@ func TestEchoURL(t *testing.T) {
 	assert.Equal("/static/file", e.URL(static))
 	assert.Equal("/users/:id", e.URL(getUser))
 	assert.Equal("/users/1", e.URL(getUser, "1"))
-	assert.Equal("/users/1", e.URL(getUser, "1"))
-	assert.Equal("/documents/foo.txt", e.URL(getAny, "foo.txt"))
-	assert.Equal("/documents/*", e.URL(getAny))
 	assert.Equal("/group/users/1/files/:fid", e.URL(getFile, "1"))
 	assert.Equal("/group/users/1/files/1", e.URL(getFile, "1", "1"))
 }
@@ -658,91 +652,3 @@ func TestEchoShutdown(t *testing.T) {
 	err := <-errCh
 	assert.Equal(t, err.Error(), "http: Server closed")
 }
-
-var listenerNetworkTests = []struct {
-	test    string
-	network string
-	address string
-}{
-	{"tcp ipv4 address", "tcp", "127.0.0.1:1323"},
-	{"tcp ipv6 address", "tcp", "[::1]:1323"},
-	{"tcp4 ipv4 address", "tcp4", "127.0.0.1:1323"},
-	{"tcp6 ipv6 address", "tcp6", "[::1]:1323"},
-}
-
-func TestEchoListenerNetwork(t *testing.T) {
-	for _, tt := range listenerNetworkTests {
-		t.Run(tt.test, func(t *testing.T) {
-			e := New()
-			e.ListenerNetwork = tt.network
-
-			// HandlerFunc
-			e.GET("/ok", func(c Context) error {
-				return c.String(http.StatusOK, "OK")
-			})
-
-			errCh := make(chan error)
-
-			go func() {
-				errCh <- e.Start(tt.address)
-			}()
-
-			time.Sleep(200 * time.Millisecond)
-
-			if resp, err := http.Get(fmt.Sprintf("http://%s/ok", tt.address)); err == nil {
-				defer resp.Body.Close()
-				assert.Equal(t, http.StatusOK, resp.StatusCode)
-
-				if body, err := ioutil.ReadAll(resp.Body); err == nil {
-					assert.Equal(t, "OK", string(body))
-				} else {
-					assert.Fail(t, err.Error())
-				}
-
-			} else {
-				assert.Fail(t, err.Error())
-			}
-
-			if err := e.Close(); err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestEchoListenerNetworkInvalid(t *testing.T) {
-	e := New()
-	e.ListenerNetwork = "unix"
-
-	// HandlerFunc
-	e.GET("/ok", func(c Context) error {
-		return c.String(http.StatusOK, "OK")
-	})
-
-	assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
-}
-
-func TestEchoReverse(t *testing.T) {
-	assert := assert.New(t)
-
-	e := New()
-	dummyHandler := func(Context) error { return nil }
-
-	e.GET("/static", dummyHandler).Name = "/static"
-	e.GET("/static/*", dummyHandler).Name = "/static/*"
-	e.GET("/params/:foo", dummyHandler).Name = "/params/:foo"
-	e.GET("/params/:foo/bar/:qux", dummyHandler).Name = "/params/:foo/bar/:qux"
-	e.GET("/params/:foo/bar/:qux/*", dummyHandler).Name = "/params/:foo/bar/:qux/*"
-
-	assert.Equal("/static", e.Reverse("/static"))
-	assert.Equal("/static", e.Reverse("/static", "missing param"))
-	assert.Equal("/static/*", e.Reverse("/static/*"))
-	assert.Equal("/static/foo.txt", e.Reverse("/static/*", "foo.txt"))
-
-	assert.Equal("/params/:foo", e.Reverse("/params/:foo"))
-	assert.Equal("/params/one", e.Reverse("/params/:foo", "one"))
-	assert.Equal("/params/:foo/bar/:qux", e.Reverse("/params/:foo/bar/:qux"))
-	assert.Equal("/params/one/bar/:qux", e.Reverse("/params/:foo/bar/:qux", "one"))
-	assert.Equal("/params/one/bar/two", e.Reverse("/params/:foo/bar/:qux", "one", "two"))
-	assert.Equal("/params/one/bar/two/three", e.Reverse("/params/:foo/bar/:qux/*", "one", "two", "three"))
-}
diff --git a/middleware/cors_test.go b/middleware/cors_test.go
index 717abe4..ca92232 100644
--- a/middleware/cors_test.go
+++ b/middleware/cors_test.go
@@ -1,7 +1,6 @@
 package middleware
 
 import (
-	"errors"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -18,31 +17,19 @@ func TestCORS(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	h := CORS()(echo.NotFoundHandler)
-	req.Header.Set(echo.HeaderOrigin, "localhost")
 	h(c)
 	assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
 
-	// Wildcard AllowedOrigin with no Origin header in request
-	req = httptest.NewRequest(http.MethodGet, "/", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h = CORS()(echo.NotFoundHandler)
-	h(c)
-	assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
-
 	// Allow origins
 	req = httptest.NewRequest(http.MethodGet, "/", nil)
 	rec = httptest.NewRecorder()
 	c = e.NewContext(req, rec)
 	h = CORSWithConfig(CORSConfig{
-		AllowOrigins:     []string{"localhost"},
-		AllowCredentials: true,
-		MaxAge:           3600,
+		AllowOrigins: []string{"localhost"},
 	})(echo.NotFoundHandler)
 	req.Header.Set(echo.HeaderOrigin, "localhost")
 	h(c)
 	assert.Equal(t, "localhost", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-	assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials))
 
 	// Preflight request
 	req = httptest.NewRequest(http.MethodOptions, "/", nil)
@@ -80,22 +67,6 @@ func TestCORS(t *testing.T) {
 	assert.Equal(t, "true", rec.Header().Get(echo.HeaderAccessControlAllowCredentials))
 	assert.Equal(t, "3600", rec.Header().Get(echo.HeaderAccessControlMaxAge))
 
-	// Preflight request with Access-Control-Request-Headers
-	req = httptest.NewRequest(http.MethodOptions, "/", nil)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	req.Header.Set(echo.HeaderOrigin, "localhost")
-	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
-	req.Header.Set(echo.HeaderAccessControlRequestHeaders, "Special-Request-Header")
-	cors = CORSWithConfig(CORSConfig{
-		AllowOrigins: []string{"*"},
-	})
-	h = cors(echo.NotFoundHandler)
-	h(c)
-	assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-	assert.Equal(t, "Special-Request-Header", rec.Header().Get(echo.HeaderAccessControlAllowHeaders))
-	assert.NotEmpty(t, rec.Header().Get(echo.HeaderAccessControlAllowMethods))
-
 	// Preflight request with `AllowOrigins` which allow all subdomains with *
 	req = httptest.NewRequest(http.MethodOptions, "/", nil)
 	rec = httptest.NewRecorder()
@@ -155,7 +126,7 @@ func Test_allowOriginScheme(t *testing.T) {
 		if tt.expected {
 			assert.Equal(t, tt.domain, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
 		} else {
-			assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
+			assert.Equal(t, "", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
 		}
 	}
 }
@@ -245,163 +216,6 @@ func Test_allowOriginSubdomain(t *testing.T) {
 
 		if tt.expected {
 			assert.Equal(t, tt.domain, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-		} else {
-			assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
-		}
-	}
-}
-
-func TestCorsHeaders(t *testing.T) {
-	tests := []struct {
-		domain, allowedOrigin, method string
-		expected                      bool
-	}{
-		{
-			domain:        "", // Request does not have Origin header
-			allowedOrigin: "*",
-			method:        http.MethodGet,
-			expected:      false,
-		},
-		{
-			domain:        "http://example.com",
-			allowedOrigin: "*",
-			method:        http.MethodGet,
-			expected:      true,
-		},
-		{
-			domain:        "", // Request does not have Origin header
-			allowedOrigin: "http://example.com",
-			method:        http.MethodGet,
-			expected:      false,
-		},
-		{
-			domain:        "http://bar.com",
-			allowedOrigin: "http://example.com",
-			method:        http.MethodGet,
-			expected:      false,
-		},
-		{
-			domain:        "http://example.com",
-			allowedOrigin: "http://example.com",
-			method:        http.MethodGet,
-			expected:      true,
-		},
-		{
-			domain:        "", // Request does not have Origin header
-			allowedOrigin: "*",
-			method:        http.MethodOptions,
-			expected:      false,
-		},
-		{
-			domain:        "http://example.com",
-			allowedOrigin: "*",
-			method:        http.MethodOptions,
-			expected:      true,
-		},
-		{
-			domain:        "", // Request does not have Origin header
-			allowedOrigin: "http://example.com",
-			method:        http.MethodOptions,
-			expected:      false,
-		},
-		{
-			domain:        "http://bar.com",
-			allowedOrigin: "http://example.com",
-			method:        http.MethodGet,
-			expected:      false,
-		},
-		{
-			domain:        "http://example.com",
-			allowedOrigin: "http://example.com",
-			method:        http.MethodOptions,
-			expected:      true,
-		},
-	}
-
-	e := echo.New()
-	for _, tt := range tests {
-		req := httptest.NewRequest(tt.method, "/", nil)
-		rec := httptest.NewRecorder()
-		c := e.NewContext(req, rec)
-		if tt.domain != "" {
-			req.Header.Set(echo.HeaderOrigin, tt.domain)
-		}
-		cors := CORSWithConfig(CORSConfig{
-			AllowOrigins: []string{tt.allowedOrigin},
-			//AllowCredentials: true,
-			//MaxAge:           3600,
-		})
-		h := cors(echo.NotFoundHandler)
-		h(c)
-
-		assert.Equal(t, echo.HeaderOrigin, rec.Header().Get(echo.HeaderVary))
-
-		expectedAllowOrigin := ""
-		if tt.allowedOrigin == "*" {
-			expectedAllowOrigin = "*"
-		} else {
-			expectedAllowOrigin = tt.domain
-		}
-
-		switch {
-		case tt.expected && tt.method == http.MethodOptions:
-			assert.Contains(t, rec.Header(), echo.HeaderAccessControlAllowMethods)
-			assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-			assert.Equal(t, 3, len(rec.Header()[echo.HeaderVary]))
-		case tt.expected && tt.method == http.MethodGet:
-			assert.Equal(t, expectedAllowOrigin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-			assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin
-		default:
-			assert.NotContains(t, rec.Header(), echo.HeaderAccessControlAllowOrigin)
-			assert.Equal(t, 1, len(rec.Header()[echo.HeaderVary])) // Vary: Origin
-		}
-
-		if tt.method == http.MethodOptions {
-			assert.Equal(t, http.StatusNoContent, rec.Code)
-		}
-	}
-}
-
-func Test_allowOriginFunc(t *testing.T) {
-	returnTrue := func(origin string) (bool, error) {
-		return true, nil
-	}
-	returnFalse := func(origin string) (bool, error) {
-		return false, nil
-	}
-	returnError := func(origin string) (bool, error) {
-		return true, errors.New("this is a test error")
-	}
-
-	allowOriginFuncs := []func(origin string) (bool, error){
-		returnTrue,
-		returnFalse,
-		returnError,
-	}
-
-	const origin = "http://example.com"
-
-	e := echo.New()
-	for _, allowOriginFunc := range allowOriginFuncs {
-		req := httptest.NewRequest(http.MethodOptions, "/", nil)
-		rec := httptest.NewRecorder()
-		c := e.NewContext(req, rec)
-		req.Header.Set(echo.HeaderOrigin, origin)
-		cors := CORSWithConfig(CORSConfig{
-			AllowOriginFunc: allowOriginFunc,
-		})
-		h := cors(echo.NotFoundHandler)
-		err := h(c)
-
-		expected, expectedErr := allowOriginFunc(origin)
-		if expectedErr != nil {
-			assert.Equal(t, expectedErr, err)
-			assert.Equal(t, "", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
-			continue
-		}
-
-		if expected {
-			assert.Equal(t, origin, rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
 		} else {
 			assert.Equal(t, "", rec.Header().Get(echo.HeaderAccessControlAllowOrigin))
 		}
diff --git a/middleware/decompress_test.go b/middleware/decompress_test.go
deleted file mode 100644
index 51fa6b0..0000000
--- a/middleware/decompress_test.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package middleware
-
-import (
-	"bytes"
-	"compress/gzip"
-	"errors"
-	"io/ioutil"
-	"net/http"
-	"net/http/httptest"
-	"strings"
-	"sync"
-	"testing"
-
-	"github.com/labstack/echo/v4"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestDecompress(t *testing.T) {
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("test"))
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-
-	// Skip if no Content-Encoding header
-	h := Decompress()(func(c echo.Context) error {
-		c.Response().Write([]byte("test")) // For Content-Type sniffing
-		return nil
-	})
-	h(c)
-
-	assert := assert.New(t)
-	assert.Equal("test", rec.Body.String())
-
-	// Decompress
-	body := `{"name": "echo"}`
-	gz, _ := gzipString(body)
-	req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h(c)
-	assert.Equal(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
-	assert.NoError(err)
-	assert.Equal(body, string(b))
-}
-
-func TestDecompressDefaultConfig(t *testing.T) {
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("test"))
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-
-	h := DecompressWithConfig(DecompressConfig{})(func(c echo.Context) error {
-		c.Response().Write([]byte("test")) // For Content-Type sniffing
-		return nil
-	})
-	h(c)
-
-	assert := assert.New(t)
-	assert.Equal("test", rec.Body.String())
-
-	// Decompress
-	body := `{"name": "echo"}`
-	gz, _ := gzipString(body)
-	req = httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec = httptest.NewRecorder()
-	c = e.NewContext(req, rec)
-	h(c)
-	assert.Equal(GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
-	assert.NoError(err)
-	assert.Equal(body, string(b))
-}
-
-func TestCompressRequestWithoutDecompressMiddleware(t *testing.T) {
-	e := echo.New()
-	body := `{"name":"echo"}`
-	gz, _ := gzipString(body)
-	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec := httptest.NewRecorder()
-	e.NewContext(req, rec)
-	e.ServeHTTP(rec, req)
-	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	b, err := ioutil.ReadAll(req.Body)
-	assert.NoError(t, err)
-	assert.NotEqual(t, b, body)
-	assert.Equal(t, b, gz)
-}
-
-func TestDecompressNoContent(t *testing.T) {
-	e := echo.New()
-	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-	h := Decompress()(func(c echo.Context) error {
-		return c.NoContent(http.StatusNoContent)
-	})
-	if assert.NoError(t, h(c)) {
-		assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-		assert.Empty(t, rec.Header().Get(echo.HeaderContentType))
-		assert.Equal(t, 0, len(rec.Body.Bytes()))
-	}
-}
-
-func TestDecompressErrorReturned(t *testing.T) {
-	e := echo.New()
-	e.Use(Decompress())
-	e.GET("/", func(c echo.Context) error {
-		return echo.ErrNotFound
-	})
-	req := httptest.NewRequest(http.MethodGet, "/", nil)
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec := httptest.NewRecorder()
-	e.ServeHTTP(rec, req)
-	assert.Equal(t, http.StatusNotFound, rec.Code)
-	assert.Empty(t, rec.Header().Get(echo.HeaderContentEncoding))
-}
-
-func TestDecompressSkipper(t *testing.T) {
-	e := echo.New()
-	e.Use(DecompressWithConfig(DecompressConfig{
-		Skipper: func(c echo.Context) bool {
-			return c.Request().URL.Path == "/skip"
-		},
-	}))
-	body := `{"name": "echo"}`
-	req := httptest.NewRequest(http.MethodPost, "/skip", strings.NewReader(body))
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-	e.ServeHTTP(rec, req)
-	assert.Equal(t, rec.Header().Get(echo.HeaderContentType), echo.MIMEApplicationJSONCharsetUTF8)
-	reqBody, err := ioutil.ReadAll(c.Request().Body)
-	assert.NoError(t, err)
-	assert.Equal(t, body, string(reqBody))
-}
-
-type TestDecompressPoolWithError struct {
-}
-
-func (d *TestDecompressPoolWithError) gzipDecompressPool() sync.Pool {
-	return sync.Pool{
-		New: func() interface{} {
-			return errors.New("pool error")
-		},
-	}
-}
-
-func TestDecompressPoolError(t *testing.T) {
-	e := echo.New()
-	e.Use(DecompressWithConfig(DecompressConfig{
-		Skipper:            DefaultSkipper,
-		GzipDecompressPool: &TestDecompressPoolWithError{},
-	}))
-	body := `{"name": "echo"}`
-	req := httptest.NewRequest(http.MethodPost, "/echo", strings.NewReader(body))
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-	rec := httptest.NewRecorder()
-	c := e.NewContext(req, rec)
-	e.ServeHTTP(rec, req)
-	assert.Equal(t, GZIPEncoding, req.Header.Get(echo.HeaderContentEncoding))
-	reqBody, err := ioutil.ReadAll(c.Request().Body)
-	assert.NoError(t, err)
-	assert.Equal(t, body, string(reqBody))
-	assert.Equal(t, rec.Code, http.StatusInternalServerError)
-}
-
-func BenchmarkDecompress(b *testing.B) {
-	e := echo.New()
-	body := `{"name": "echo"}`
-	gz, _ := gzipString(body)
-	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(gz)))
-	req.Header.Set(echo.HeaderContentEncoding, GZIPEncoding)
-
-	h := Decompress()(func(c echo.Context) error {
-		c.Response().Write([]byte(body)) // For Content-Type sniffing
-		return nil
-	})
-
-	b.ReportAllocs()
-	b.ResetTimer()
-
-	for i := 0; i < b.N; i++ {
-		// Decompress
-		rec := httptest.NewRecorder()
-		c := e.NewContext(req, rec)
-		h(c)
-	}
-}
-
-func gzipString(body string) ([]byte, error) {
-	var buf bytes.Buffer
-	gz := gzip.NewWriter(&buf)
-
-	_, err := gz.Write([]byte(body))
-	if err != nil {
-		return nil, err
-	}
-
-	if err := gz.Close(); err != nil {
-		return nil, err
-	}
-
-	return buf.Bytes(), nil
-}
diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go
index 205721a..ce44f9c 100644
--- a/middleware/jwt_test.go
+++ b/middleware/jwt_test.go
@@ -3,8 +3,6 @@ package middleware
 import (
 	"net/http"
 	"net/http/httptest"
-	"net/url"
-	"strings"
 	"testing"
 
 	"github.com/dgrijalva/jwt-go"
@@ -77,7 +75,6 @@ func TestJWT(t *testing.T) {
 		reqURL     string // "/" if empty
 		hdrAuth    string
 		hdrCookie  string // test.Request doesn't provide SetCookie(); use name=val
-		formValues map[string]string
 		info       string
 	}{
 		{
@@ -195,48 +192,12 @@ func TestJWT(t *testing.T) {
 			expErrCode: http.StatusBadRequest,
 			info:       "Empty cookie",
 		},
-		{
-			config: JWTConfig{
-				SigningKey:  validKey,
-				TokenLookup: "form:jwt",
-			},
-			formValues: map[string]string{"jwt": token},
-			info:       "Valid form method",
-		},
-		{
-			config: JWTConfig{
-				SigningKey:  validKey,
-				TokenLookup: "form:jwt",
-			},
-			expErrCode: http.StatusUnauthorized,
-			formValues: map[string]string{"jwt": "invalid"},
-			info:       "Invalid token with form method",
-		},
-		{
-			config: JWTConfig{
-				SigningKey:  validKey,
-				TokenLookup: "form:jwt",
-			},
-			expErrCode: http.StatusBadRequest,
-			info:       "Empty form field",
-		},
 	} {
 		if tc.reqURL == "" {
 			tc.reqURL = "/"
 		}
 
-		var req *http.Request
-		if len(tc.formValues) > 0 {
-			form := url.Values{}
-			for k, v := range tc.formValues {
-				form.Set(k, v)
-			}
-			req = httptest.NewRequest(http.MethodPost, tc.reqURL, strings.NewReader(form.Encode()))
-			req.Header.Set(echo.HeaderContentType, "application/x-www-form-urlencoded")
-			req.ParseForm()
-		} else {
-			req = httptest.NewRequest(http.MethodGet, tc.reqURL, nil)
-		}
+		req := httptest.NewRequest(http.MethodGet, tc.reqURL, nil)
 		res := httptest.NewRecorder()
 		req.Header.Set(echo.HeaderAuthorization, tc.hdrAuth)
 		req.Header.Set(echo.HeaderCookie, tc.hdrCookie)
diff --git a/middleware/static_test.go b/middleware/static_test.go
index 407dd15..0d695d3 100644
--- a/middleware/static_test.go
+++ b/middleware/static_test.go
@@ -3,7 +3,6 @@ package middleware
 import (
 	"net/http"
 	"net/http/httptest"
-	"path/filepath"
 	"testing"
 
 	"github.com/labstack/echo/v4"
@@ -68,27 +67,4 @@ func TestStatic(t *testing.T) {
 		assert.Equal(http.StatusOK, rec.Code)
 		assert.Contains(rec.Body.String(), "cert.pem")
 	}
-
-	// IgnoreBase
-	req = httptest.NewRequest(http.MethodGet, "/_fixture", nil)
-	rec = httptest.NewRecorder()
-	config.Root = "../_fixture"
-	config.IgnoreBase = true
-	static = StaticWithConfig(config)
-	c.Echo().Group("_fixture", static)
-	e.ServeHTTP(rec, req)
-
-	assert.Equal(http.StatusOK, rec.Code)
-	assert.Equal(rec.Header().Get(echo.HeaderContentLength), "122")
-
-	req = httptest.NewRequest(http.MethodGet, "/_fixture", nil)
-	rec = httptest.NewRecorder()
-	config.Root = "../_fixture"
-	config.IgnoreBase = false
-	static = StaticWithConfig(config)
-	c.Echo().Group("_fixture", static)
-	e.ServeHTTP(rec, req)
-
-	assert.Equal(http.StatusOK, rec.Code)
-	assert.Contains(rec.Body.String(), filepath.Join("..", "_fixture", "_fixture"))
 }
diff --git a/response_test.go b/response_test.go
index d95e079..7a9c51c 100644
--- a/response_test.go
+++ b/response_test.go
@@ -56,19 +56,3 @@ func TestResponse_Flush(t *testing.T) {
 	res.Flush()
 	assert.True(t, rec.Flushed)
 }
-
-func TestResponse_ChangeStatusCodeBeforeWrite(t *testing.T) {
-	e := New()
-	rec := httptest.NewRecorder()
-	res := &Response{echo: e, Writer: rec}
-
-	res.Before(func() {
-		if 200 < res.Status && res.Status < 300 {
-			res.Status = 200
-		}
-	})
-
-	res.WriteHeader(209)
-
-	assert.Equal(t, http.StatusOK, rec.Code)
-}
diff --git a/router_test.go b/router_test.go
index fca3a79..0e88323 100644
--- a/router_test.go
+++ b/router_test.go
@@ -1298,40 +1298,6 @@ func TestRouterParam1466(t *testing.T) {
 	assert.Equal(t, 0, c.response.Status)
 }
 
-// Issue #1653
-func TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {
-	e := New()
-	r := e.router
-
-	r.Add(http.MethodGet, "/users/create", handlerHelper("create", 1))
-	r.Add(http.MethodGet, "/users/:id/edit", func(c Context) error {
-		return nil
-	})
-	r.Add(http.MethodGet, "/users/:id/active", func(c Context) error {
-		return nil
-	})
-
-	c := e.NewContext(nil, nil).(*context)
-	r.Find(http.MethodGet, "/users/alice/edit", c)
-	assert.Equal(t, "alice", c.Param("id"))
-
-	c = e.NewContext(nil, nil).(*context)
-	r.Find(http.MethodGet, "/users/bob/active", c)
-	assert.Equal(t, "bob", c.Param("id"))
-
-	c = e.NewContext(nil, nil).(*context)
-	r.Find(http.MethodGet, "/users/create", c)
-	c.Handler()(c)
-	assert.Equal(t, 1, c.Get("create"))
-	assert.Equal(t, "/users/create", c.Get("path"))
-
-	//This panic before the fix for Issue #1653
-	c = e.NewContext(nil, nil).(*context)
-	r.Find(http.MethodGet, "/users/createNotFound", c)
-	he := c.Handler()(c).(*HTTPError)
-	assert.Equal(t, http.StatusNotFound, he.Code)
-}
-
 func benchmarkRouterRoutes(b *testing.B, routes []*Route) {
 	e := New()
 	r := e.router