Repository: labstack/echo
Pull request #1736 (2020-12-28 23:18:33)

Fluent Binder for Query/Path/Form binding (#1717)
Proposed implementation for #1717 

---------

Following functions provide handful of methods for binding to Go native types from request query or path parameters.
* QueryParamsBinder(c) - binds query parameters (source URL)
* PathParamsBinder(c) - binds path parameters (source URL)
* FormFieldBinder(c) - binds form fields (source URL + body)

Example:
```go
var opts struct {
	IDs []int64
	Active bool
}
length := int64(50) // default length is 50

// creates query params binder that stops binding at first error
err := echo.QueryParamsBinder(c).
	Int64("length", &length).
	Int64s("ids", &opts.IDs).
	Bool("active", &opts.Active).
	BindError() // returns first binding error
```

For every supported type there are following methods:
* `<Type>("param", &destination)` - if parameter value exists then binds it to given destination of that type i.e Int64(...).
* `Must<Type>("param", &destination)` - parameter value is required to exist, binds it to given destination of that type i.e MustInt64(...).
* `<Type>s("param", &destination)` - (for slices) if parameter values exists then binds it to given destination of that type i.e Int64s(...).
* `Must<Type>s("param", &destination)` - (for slices) parameter value is required to exist, binds it to given destination of that type i.e MustInt64s(...).

for some slice types `BindWithDelimiter("param", &dest, ",")` supports slitting parameter values before type conversion is done
i.e. URL `/api/search?id=1,2,3&id=1` can be bind to `[]int64{1,2,3,1}`

`FailFast` flags binder to stop binding after first bind error during binder call chain. Enabled by default.
`BindError()` returns first bind error from binder and resets errors in binder. Useful along with `FailFast()` method
to do binding and returns on first problem
`BindErrors()` returns all bind errors from binder and resets errors in binder.

Types that are supported:
* bool
* float32
* float64
* int
* int8
* int16
* int32
* int64
* uint
* uint8/byte (does not support `bytes()`. Use BindUnmarshaler/CustomFunc to convert value from base64 etc to []byte{})
* uint16
* uint32
* uint64
* string
* time
* duration
* BindUnmarshaler() interface
* UnixTime() - converts unix time (integer) to time.Time
* CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error`



Added some naive benchmarks to compare different implementations (query params)

```
BenchmarkDefaultBinder_BindInt64_single-6        4744263               252 ns/op              16 B/op          2 allocs/op
BenchmarkValueBinder_BindInt64_single-6         43042522                27.9 ns/op             0 B/op          0 allocs/op
BenchmarkRawFunc_Int64_single-6                 83911664                14.2 ns/op             0 B/op          0 allocs/op

BenchmarkDefaultBinder_BindInt64_10_fields-6      490051              2319 ns/op             224 B/op         13 allocs/op
BenchmarkValueBinder_BindInt64_10_fields-6       3793227               319 ns/op               0 B/op          0 allocs/op
```

* `BenchmarkDefaultBinder_BindInt64_single` uses DefaultBinder to bind single int64 value
* `BenchmarkValueBinder_BindInt64_single` uses new binder to bind single int64 value
* `BenchmarkRawFunc_Int64_single` just gets param from contexts, parses it with utility method and sets to variable
* `BenchmarkDefaultBinder_BindInt64_10_fields` uses DefaultBinder to bind 10 fields to struct
* `BenchmarkValueBinder_BindInt64_10_fields` uses new binder to bind 10 fields to struct

@lammel  @pafuent what do you think?


Patch:
diff --git a/.github/workflows/echo.yml b/.github/workflows/echo.yml
index 2aec272..fb8c502 100644
--- a/.github/workflows/echo.yml
+++ b/.github/workflows/echo.yml
@@ -59,7 +59,7 @@ jobs:
           go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
 
       - name: Upload coverage to Codecov
-        if: success() && matrix.go == 1.13 && matrix.os == 'ubuntu-latest'
+        if: success() && matrix.go == 1.15 && matrix.os == 'ubuntu-latest'
         uses: codecov/codecov-action@v1
         with:
           token:
diff --git a/Makefile b/Makefile
index c369913..bedb8bd 100644
--- a/Makefile
+++ b/Makefile
@@ -23,5 +23,12 @@ test: ## Run tests
 race: ## Run tests with data race detector
 	@go test -race ${PKG_LIST}
 
+benchmark: ## Run benchmarks
+	@go test -run="-" -bench=".*" ${PKG_LIST}
+
 help: ## Display this help screen
 	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+goversion ?= "1.12"
+test_version: ## Run tests inside Docker with given version (defaults to 1.12 oldest supported). Example: make test_version goversion=1.13
+	@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make check"
diff --git a/README.md b/README.md
index deba54f..4dec531 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo)
 [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo)
 [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
-[![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://forum.labstack.com)
+[![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)
 
@@ -94,7 +94,7 @@ func hello(c echo.Context) error {
 
 ## Help
 
-- [Forum](https://forum.labstack.com)
+- [Forum](https://github.com/labstack/echo/discussions)
 - [Chat](https://gitter.im/labstack/echo)
 
 ## Contribute
diff --git a/bind.go b/bind.go
index acd2bed..16c3b7a 100644
--- a/bind.go
+++ b/bind.go
@@ -116,12 +116,13 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
 	return b.BindBody(c, i)
 }
 
-func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
-	if ptr == nil || len(data) == 0 {
+// bindData will bind data ONLY fields in destination struct that have EXPLICIT tag
+func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string) error {
+	if destination == nil || len(data) == 0 {
 		return nil
 	}
-	typ := reflect.TypeOf(ptr).Elem()
-	val := reflect.ValueOf(ptr).Elem()
+	typ := reflect.TypeOf(destination).Elem()
+	val := reflect.ValueOf(destination).Elem()
 
 	// Map
 	if typ.Kind() == reflect.Map {
@@ -146,14 +147,15 @@ func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag
 		inputFieldName := typeField.Tag.Get(tag)
 
 		if inputFieldName == "" {
-			inputFieldName = typeField.Name
-			// If tag is nil, we inspect if the field is a struct.
+			// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags).
+			// structs that implement BindUnmarshaler are binded only when they have explicit tag
 			if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
 				if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
 					return err
 				}
-				continue
 			}
+			// does not have explicit tag and is not an ordinary struct - so move to next field
+			continue
 		}
 
 		inputValue, exists := data[inputFieldName]
diff --git a/binder.go b/binder.go
new file mode 100644
index 0000000..9f0ca65
--- /dev/null
+++ b/binder.go
@@ -0,0 +1,1234 @@
+package echo
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+)
+
+/**
+	Following functions provide handful of methods for binding to Go native types from request query or path parameters.
+    * QueryParamsBinder(c) - binds query parameters (source URL)
+    * PathParamsBinder(c) - binds path parameters (source URL)
+    * FormFieldBinder(c) - binds form fields (source URL + body)
+
+	Example:
+  ```go
+  var length int64
+  err := echo.QueryParamsBinder(c).Int64("length", &length).BindError()
+  ```
+
+	For every supported type there are following methods:
+		* <Type>("param", &destination) - if parameter value exists then binds it to given destination of that type i.e Int64(...).
+		* Must<Type>("param", &destination) - parameter value is required to exist, binds it to given destination of that type i.e MustInt64(...).
+		* <Type>s("param", &destination) - (for slices) if parameter values exists then binds it to given destination of that type i.e Int64s(...).
+		* Must<Type>s("param", &destination) - (for slices) parameter value is required to exist, binds it to given destination of that type i.e MustInt64s(...).
+
+  for some slice types `BindWithDelimiter("param", &dest, ",")` supports splitting parameter values before type conversion is done
+  i.e. URL `/api/search?id=1,2,3&id=1` can be bind to `[]int64{1,2,3,1}`
+
+	`FailFast` flags binder to stop binding after first bind error during binder call chain. Enabled by default.
+  `BindError()` returns first bind error from binder and resets errors in binder. Useful along with `FailFast()` method
+		to do binding and returns on first problem
+  `BindErrors()` returns all bind errors from binder and resets errors in binder.
+
+	Types that are supported:
+		* bool
+		* float32
+		* float64
+		* int
+		* int8
+		* int16
+		* int32
+		* int64
+		* uint
+		* uint8/byte (does not support `bytes()`. Use BindUnmarshaler/CustomFunc to convert value from base64 etc to []byte{})
+		* uint16
+		* uint32
+		* uint64
+		* string
+		* time
+		* duration
+		* BindUnmarshaler() interface
+		* UnixTime() - converts unix time (integer) to time.Time
+		* UnixTimeNano() - converts unix time with nano second precision (integer) to time.Time
+		* CustomFunc() - callback function for your custom conversion logic. Signature `func(values []string) []error`
+*/
+
+// BindingError represents an error that occurred while binding request data.
+type BindingError struct {
+	// Field is the field name where value binding failed
+	Field string `json:"field"`
+	// Values of parameter that failed to bind.
+	Values []string `json:"-"`
+	*HTTPError
+}
+
+// NewBindingError creates new instance of binding error
+func NewBindingError(sourceParam string, values []string, message interface{}, internalError error) error {
+	return &BindingError{
+		Field:  sourceParam,
+		Values: values,
+		HTTPError: &HTTPError{
+			Code:     http.StatusBadRequest,
+			Message:  message,
+			Internal: internalError,
+		},
+	}
+}
+
+// Error returns error message
+func (be *BindingError) Error() string {
+	return fmt.Sprintf("%s, field=%s", be.HTTPError.Error(), be.Field)
+}
+
+// ValueBinder provides utility methods for binding query or path parameter to various Go built-in types
+type ValueBinder struct {
+	// failFast is flag for binding methods to return without attempting to bind when previous binding already failed
+	failFast bool
+	errors   []error
+
+	// ValueFunc is used to get single parameter (first) value from request
+	ValueFunc func(sourceParam string) string
+	// ValuesFunc is used to get all values for parameter from request. i.e. `/api/search?ids=1&ids=2`
+	ValuesFunc func(sourceParam string) []string
+	// ErrorFunc is used to create errors. Allows you to use your own error type, that for example marshals to your specific json response
+	ErrorFunc func(sourceParam string, values []string, message interface{}, internalError error) error
+}
+
+// QueryParamsBinder creates query parameter value binder
+func QueryParamsBinder(c Context) *ValueBinder {
+	return &ValueBinder{
+		failFast: true,
+		ValueFunc: func(sourceParam string) string {
+			return c.QueryParam(sourceParam)
+		},
+		ValuesFunc: func(sourceParam string) []string {
+			values, ok := c.QueryParams()[sourceParam]
+			if !ok {
+				return nil
+			}
+			return values
+		},
+		ErrorFunc: NewBindingError,
+	}
+}
+
+// PathParamsBinder creates path parameter value binder
+func PathParamsBinder(c Context) *ValueBinder {
+	return &ValueBinder{
+		failFast: true,
+		ValueFunc: func(sourceParam string) string {
+			return c.Param(sourceParam)
+		},
+		ValuesFunc: func(sourceParam string) []string {
+			// path parameter should not have multiple values so getting values does not make sense but lets not error out here
+			value := c.Param(sourceParam)
+			if value == "" {
+				return nil
+			}
+			return []string{value}
+		},
+		ErrorFunc: NewBindingError,
+	}
+}
+
+// FormFieldBinder creates form field value binder
+// For all requests, FormFieldBinder parses the raw query from the URL and uses query params as form fields
+//
+// For POST, PUT, and PATCH requests, it also reads the request body, parses it
+// as a form and uses query params as form fields. Request body parameters take precedence over URL query
+// string values in r.Form.
+//
+// NB: when 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 https://golang.org/pkg/net/http/#Request.ParseForm
+func FormFieldBinder(c Context) *ValueBinder {
+	vb := &ValueBinder{
+		failFast: true,
+		ValueFunc: func(sourceParam string) string {
+			return c.Request().FormValue(sourceParam)
+		},
+		ErrorFunc: NewBindingError,
+	}
+	vb.ValuesFunc = func(sourceParam string) []string {
+		if c.Request().Form == nil {
+			// this is same as `Request().FormValue()` does internally
+			_ = c.Request().ParseMultipartForm(32 << 20)
+		}
+		values, ok := c.Request().Form[sourceParam]
+		if !ok {
+			return nil
+		}
+		return values
+	}
+
+	return vb
+}
+
+// FailFast set internal flag to indicate if binding methods will return early (without binding) when previous bind failed
+// NB: call this method before any other binding methods as it modifies binding methods behaviour
+func (b *ValueBinder) FailFast(value bool) *ValueBinder {
+	b.failFast = value
+	return b
+}
+
+func (b *ValueBinder) setError(err error) {
+	if b.errors == nil {
+		b.errors = []error{err}
+		return
+	}
+	b.errors = append(b.errors, err)
+}
+
+// BindError returns first seen bind error and resets/empties binder errors for further calls
+func (b *ValueBinder) BindError() error {
+	if b.errors == nil {
+		return nil
+	}
+	err := b.errors[0]
+	b.errors = nil // reset errors so next chain will start from zero
+	return err
+}
+
+// BindErrors returns all bind errors and resets/empties binder errors for further calls
+func (b *ValueBinder) BindErrors() []error {
+	if b.errors == nil {
+		return nil
+	}
+	errors := b.errors
+	b.errors = nil // reset errors so next chain will start from zero
+	return errors
+}
+
+// CustomFunc binds parameter values with Func. Func is called only when parameter values exist.
+func (b *ValueBinder) CustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder {
+	return b.customFunc(sourceParam, customFunc, false)
+}
+
+// MustCustomFunc requires parameter values to exist to be bind with Func. Returns error when value does not exist.
+func (b *ValueBinder) MustCustomFunc(sourceParam string, customFunc func(values []string) []error) *ValueBinder {
+	return b.customFunc(sourceParam, customFunc, true)
+}
+
+func (b *ValueBinder) customFunc(sourceParam string, customFunc func(values []string) []error, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+	if errs := customFunc(values); errs != nil {
+		b.errors = append(b.errors, errs...)
+	}
+	return b
+}
+
+// String binds parameter to string variable
+func (b *ValueBinder) String(sourceParam string, dest *string) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		return b
+	}
+	*dest = value
+	return b
+}
+
+// MustString requires parameter value to exist to be bind to string variable. Returns error when value does not exist
+func (b *ValueBinder) MustString(sourceParam string, dest *string) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil))
+		return b
+	}
+	*dest = value
+	return b
+}
+
+// Strings binds parameter values to slice of string
+func (b *ValueBinder) Strings(sourceParam string, dest *[]string) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValuesFunc(sourceParam)
+	if value == nil {
+		return b
+	}
+	*dest = value
+	return b
+}
+
+// MustStrings requires parameter values to exist to be bind to slice of string variables. Returns error when value does not exist
+func (b *ValueBinder) MustStrings(sourceParam string, dest *[]string) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValuesFunc(sourceParam)
+	if value == nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		return b
+	}
+	*dest = value
+	return b
+}
+
+// BindUnmarshaler binds parameter to destination implementing BindUnmarshaler interface
+func (b *ValueBinder) BindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	tmp := b.ValueFunc(sourceParam)
+	if tmp == "" {
+		return b
+	}
+
+	if err := dest.UnmarshalParam(tmp); err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{tmp}, "failed to bind field value to BindUnmarshaler interface", err))
+	}
+	return b
+}
+
+// MustBindUnmarshaler requires parameter value to exist to be bind to destination implementing BindUnmarshaler interface.
+// Returns error when value does not exist
+func (b *ValueBinder) MustBindUnmarshaler(sourceParam string, dest BindUnmarshaler) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil))
+		return b
+	}
+
+	if err := dest.UnmarshalParam(value); err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to BindUnmarshaler interface", err))
+	}
+	return b
+}
+
+// BindWithDelimiter binds parameter to destination by suitable conversion function.
+// Delimiter is used before conversion to split parameter value to separate values
+func (b *ValueBinder) BindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder {
+	return b.bindWithDelimiter(sourceParam, dest, delimiter, false)
+}
+
+// MustBindWithDelimiter requires parameter value to exist to be bind destination by suitable conversion function.
+// Delimiter is used before conversion to split parameter value to separate values
+func (b *ValueBinder) MustBindWithDelimiter(sourceParam string, dest interface{}, delimiter string) *ValueBinder {
+	return b.bindWithDelimiter(sourceParam, dest, delimiter, true)
+}
+
+func (b *ValueBinder) bindWithDelimiter(sourceParam string, dest interface{}, delimiter string, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+	tmpValues := make([]string, 0, len(values))
+	for _, v := range values {
+		tmpValues = append(tmpValues, strings.Split(v, delimiter)...)
+	}
+
+	switch d := dest.(type) {
+	case *[]string:
+		*d = tmpValues
+		return b
+	case *[]bool:
+		return b.bools(sourceParam, tmpValues, d)
+	case *[]int64, *[]int32, *[]int16, *[]int8, *[]int:
+		return b.ints(sourceParam, tmpValues, d)
+	case *[]uint64, *[]uint32, *[]uint16, *[]uint8, *[]uint: // *[]byte is same as *[]uint8
+		return b.uints(sourceParam, tmpValues, d)
+	case *[]float64, *[]float32:
+		return b.floats(sourceParam, tmpValues, d)
+	case *[]time.Duration:
+		return b.durations(sourceParam, tmpValues, d)
+	default:
+		// support only cases when destination is slice
+		// does not support time.Time as it needs argument (layout) for parsing or BindUnmarshaler
+		b.setError(b.ErrorFunc(sourceParam, []string{}, "unsupported bind type", nil))
+		return b
+	}
+}
+
+// Int64 binds parameter to int64 variable
+func (b *ValueBinder) Int64(sourceParam string, dest *int64) *ValueBinder {
+	return b.intValue(sourceParam, dest, 64, false)
+}
+
+// MustInt64 requires parameter value to exist to be bind to int64 variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt64(sourceParam string, dest *int64) *ValueBinder {
+	return b.intValue(sourceParam, dest, 64, true)
+}
+
+// Int32 binds parameter to int32 variable
+func (b *ValueBinder) Int32(sourceParam string, dest *int32) *ValueBinder {
+	return b.intValue(sourceParam, dest, 32, false)
+}
+
+// MustInt32 requires parameter value to exist to be bind to int32 variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt32(sourceParam string, dest *int32) *ValueBinder {
+	return b.intValue(sourceParam, dest, 32, true)
+}
+
+// Int16 binds parameter to int16 variable
+func (b *ValueBinder) Int16(sourceParam string, dest *int16) *ValueBinder {
+	return b.intValue(sourceParam, dest, 16, false)
+}
+
+// MustInt16 requires parameter value to exist to be bind to int16 variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt16(sourceParam string, dest *int16) *ValueBinder {
+	return b.intValue(sourceParam, dest, 16, true)
+}
+
+// Int8 binds parameter to int8 variable
+func (b *ValueBinder) Int8(sourceParam string, dest *int8) *ValueBinder {
+	return b.intValue(sourceParam, dest, 8, false)
+}
+
+// MustInt8 requires parameter value to exist to be bind to int8 variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt8(sourceParam string, dest *int8) *ValueBinder {
+	return b.intValue(sourceParam, dest, 8, true)
+}
+
+// Int binds parameter to int variable
+func (b *ValueBinder) Int(sourceParam string, dest *int) *ValueBinder {
+	return b.intValue(sourceParam, dest, 0, false)
+}
+
+// MustInt requires parameter value to exist to be bind to int variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt(sourceParam string, dest *int) *ValueBinder {
+	return b.intValue(sourceParam, dest, 0, true)
+}
+
+func (b *ValueBinder) intValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+
+	return b.int(sourceParam, value, dest, bitSize)
+}
+
+func (b *ValueBinder) int(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder {
+	n, err := strconv.ParseInt(value, 10, bitSize)
+	if err != nil {
+		if bitSize == 0 {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to int", err))
+		} else {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to int%v", bitSize), err))
+		}
+		return b
+	}
+
+	switch d := dest.(type) {
+	case *int64:
+		*d = n
+	case *int32:
+		*d = int32(n)
+	case *int16:
+		*d = int16(n)
+	case *int8:
+		*d = int8(n)
+	case *int:
+		*d = int(n)
+	}
+	return b
+}
+
+func (b *ValueBinder) intsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, values, "required field value is empty", nil))
+		}
+		return b
+	}
+	return b.ints(sourceParam, values, dest)
+}
+
+func (b *ValueBinder) ints(sourceParam string, values []string, dest interface{}) *ValueBinder {
+	switch d := dest.(type) {
+	case *[]int64:
+		tmp := make([]int64, len(values))
+		for i, v := range values {
+			b.int(sourceParam, v, &tmp[i], 64)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]int32:
+		tmp := make([]int32, len(values))
+		for i, v := range values {
+			b.int(sourceParam, v, &tmp[i], 32)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]int16:
+		tmp := make([]int16, len(values))
+		for i, v := range values {
+			b.int(sourceParam, v, &tmp[i], 16)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]int8:
+		tmp := make([]int8, len(values))
+		for i, v := range values {
+			b.int(sourceParam, v, &tmp[i], 8)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]int:
+		tmp := make([]int, len(values))
+		for i, v := range values {
+			b.int(sourceParam, v, &tmp[i], 0)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	}
+	return b
+}
+
+// Int64s binds parameter to slice of int64
+func (b *ValueBinder) Int64s(sourceParam string, dest *[]int64) *ValueBinder {
+	return b.intsValue(sourceParam, dest, false)
+}
+
+// MustInt64s requires parameter value to exist to be bind to int64 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt64s(sourceParam string, dest *[]int64) *ValueBinder {
+	return b.intsValue(sourceParam, dest, true)
+}
+
+// Int32s binds parameter to slice of int32
+func (b *ValueBinder) Int32s(sourceParam string, dest *[]int32) *ValueBinder {
+	return b.intsValue(sourceParam, dest, false)
+}
+
+// MustInt32s requires parameter value to exist to be bind to int32 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt32s(sourceParam string, dest *[]int32) *ValueBinder {
+	return b.intsValue(sourceParam, dest, true)
+}
+
+// Int16s binds parameter to slice of int16
+func (b *ValueBinder) Int16s(sourceParam string, dest *[]int16) *ValueBinder {
+	return b.intsValue(sourceParam, dest, false)
+}
+
+// MustInt16s requires parameter value to exist to be bind to int16 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt16s(sourceParam string, dest *[]int16) *ValueBinder {
+	return b.intsValue(sourceParam, dest, true)
+}
+
+// Int8s binds parameter to slice of int8
+func (b *ValueBinder) Int8s(sourceParam string, dest *[]int8) *ValueBinder {
+	return b.intsValue(sourceParam, dest, false)
+}
+
+// MustInt8s requires parameter value to exist to be bind to int8 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustInt8s(sourceParam string, dest *[]int8) *ValueBinder {
+	return b.intsValue(sourceParam, dest, true)
+}
+
+// Ints binds parameter to slice of int
+func (b *ValueBinder) Ints(sourceParam string, dest *[]int) *ValueBinder {
+	return b.intsValue(sourceParam, dest, false)
+}
+
+// MustInts requires parameter value to exist to be bind to int slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustInts(sourceParam string, dest *[]int) *ValueBinder {
+	return b.intsValue(sourceParam, dest, true)
+}
+
+// Uint64 binds parameter to uint64 variable
+func (b *ValueBinder) Uint64(sourceParam string, dest *uint64) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 64, false)
+}
+
+// MustUint64 requires parameter value to exist to be bind to uint64 variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint64(sourceParam string, dest *uint64) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 64, true)
+}
+
+// Uint32 binds parameter to uint32 variable
+func (b *ValueBinder) Uint32(sourceParam string, dest *uint32) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 32, false)
+}
+
+// MustUint32 requires parameter value to exist to be bind to uint32 variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint32(sourceParam string, dest *uint32) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 32, true)
+}
+
+// Uint16 binds parameter to uint16 variable
+func (b *ValueBinder) Uint16(sourceParam string, dest *uint16) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 16, false)
+}
+
+// MustUint16 requires parameter value to exist to be bind to uint16 variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint16(sourceParam string, dest *uint16) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 16, true)
+}
+
+// Uint8 binds parameter to uint8 variable
+func (b *ValueBinder) Uint8(sourceParam string, dest *uint8) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 8, false)
+}
+
+// MustUint8 requires parameter value to exist to be bind to uint8 variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint8(sourceParam string, dest *uint8) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 8, true)
+}
+
+// Byte binds parameter to byte variable
+func (b *ValueBinder) Byte(sourceParam string, dest *byte) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 8, false)
+}
+
+// MustByte requires parameter value to exist to be bind to byte variable. Returns error when value does not exist
+func (b *ValueBinder) MustByte(sourceParam string, dest *byte) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 8, true)
+}
+
+// Uint binds parameter to uint variable
+func (b *ValueBinder) Uint(sourceParam string, dest *uint) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 0, false)
+}
+
+// MustUint requires parameter value to exist to be bind to uint variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint(sourceParam string, dest *uint) *ValueBinder {
+	return b.uintValue(sourceParam, dest, 0, true)
+}
+
+func (b *ValueBinder) uintValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+
+	return b.uint(sourceParam, value, dest, bitSize)
+}
+
+func (b *ValueBinder) uint(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder {
+	n, err := strconv.ParseUint(value, 10, bitSize)
+	if err != nil {
+		if bitSize == 0 {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to uint", err))
+		} else {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to uint%v", bitSize), err))
+		}
+		return b
+	}
+
+	switch d := dest.(type) {
+	case *uint64:
+		*d = n
+	case *uint32:
+		*d = uint32(n)
+	case *uint16:
+		*d = uint16(n)
+	case *uint8: // byte is alias to uint8
+		*d = uint8(n)
+	case *uint:
+		*d = uint(n)
+	}
+	return b
+}
+
+func (b *ValueBinder) uintsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, values, "required field value is empty", nil))
+		}
+		return b
+	}
+	return b.uints(sourceParam, values, dest)
+}
+
+func (b *ValueBinder) uints(sourceParam string, values []string, dest interface{}) *ValueBinder {
+	switch d := dest.(type) {
+	case *[]uint64:
+		tmp := make([]uint64, len(values))
+		for i, v := range values {
+			b.uint(sourceParam, v, &tmp[i], 64)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]uint32:
+		tmp := make([]uint32, len(values))
+		for i, v := range values {
+			b.uint(sourceParam, v, &tmp[i], 32)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]uint16:
+		tmp := make([]uint16, len(values))
+		for i, v := range values {
+			b.uint(sourceParam, v, &tmp[i], 16)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]uint8: // byte is alias to uint8
+		tmp := make([]uint8, len(values))
+		for i, v := range values {
+			b.uint(sourceParam, v, &tmp[i], 8)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]uint:
+		tmp := make([]uint, len(values))
+		for i, v := range values {
+			b.uint(sourceParam, v, &tmp[i], 0)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	}
+	return b
+}
+
+// Uint64s binds parameter to slice of uint64
+func (b *ValueBinder) Uint64s(sourceParam string, dest *[]uint64) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, false)
+}
+
+// MustUint64s requires parameter value to exist to be bind to uint64 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint64s(sourceParam string, dest *[]uint64) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, true)
+}
+
+// Uint32s binds parameter to slice of uint32
+func (b *ValueBinder) Uint32s(sourceParam string, dest *[]uint32) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, false)
+}
+
+// MustUint32s requires parameter value to exist to be bind to uint32 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint32s(sourceParam string, dest *[]uint32) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, true)
+}
+
+// Uint16s binds parameter to slice of uint16
+func (b *ValueBinder) Uint16s(sourceParam string, dest *[]uint16) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, false)
+}
+
+// MustUint16s requires parameter value to exist to be bind to uint16 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint16s(sourceParam string, dest *[]uint16) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, true)
+}
+
+// Uint8s binds parameter to slice of uint8
+func (b *ValueBinder) Uint8s(sourceParam string, dest *[]uint8) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, false)
+}
+
+// MustUint8s requires parameter value to exist to be bind to uint8 slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustUint8s(sourceParam string, dest *[]uint8) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, true)
+}
+
+// Uints binds parameter to slice of uint
+func (b *ValueBinder) Uints(sourceParam string, dest *[]uint) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, false)
+}
+
+// MustUints requires parameter value to exist to be bind to uint slice variable. Returns error when value does not exist
+func (b *ValueBinder) MustUints(sourceParam string, dest *[]uint) *ValueBinder {
+	return b.uintsValue(sourceParam, dest, true)
+}
+
+// Bool binds parameter to bool variable
+func (b *ValueBinder) Bool(sourceParam string, dest *bool) *ValueBinder {
+	return b.boolValue(sourceParam, dest, false)
+}
+
+// MustBool requires parameter value to exist to be bind to bool variable. Returns error when value does not exist
+func (b *ValueBinder) MustBool(sourceParam string, dest *bool) *ValueBinder {
+	return b.boolValue(sourceParam, dest, true)
+}
+
+func (b *ValueBinder) boolValue(sourceParam string, dest *bool, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+	return b.bool(sourceParam, value, dest)
+}
+
+func (b *ValueBinder) bool(sourceParam string, value string, dest *bool) *ValueBinder {
+	n, err := strconv.ParseBool(value)
+	if err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to bool", err))
+		return b
+	}
+
+	*dest = n
+	return b
+}
+
+func (b *ValueBinder) boolsValue(sourceParam string, dest *[]bool, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+	return b.bools(sourceParam, values, dest)
+}
+
+func (b *ValueBinder) bools(sourceParam string, values []string, dest *[]bool) *ValueBinder {
+	tmp := make([]bool, len(values))
+	for i, v := range values {
+		b.bool(sourceParam, v, &tmp[i])
+		if b.failFast && b.errors != nil {
+			return b
+		}
+	}
+	if b.errors == nil {
+		*dest = tmp
+	}
+	return b
+}
+
+// Bools binds parameter values to slice of bool variables
+func (b *ValueBinder) Bools(sourceParam string, dest *[]bool) *ValueBinder {
+	return b.boolsValue(sourceParam, dest, false)
+}
+
+// MustBools requires parameter values to exist to be bind to slice of bool variables. Returns error when values does not exist
+func (b *ValueBinder) MustBools(sourceParam string, dest *[]bool) *ValueBinder {
+	return b.boolsValue(sourceParam, dest, true)
+}
+
+// Float64 binds parameter to float64 variable
+func (b *ValueBinder) Float64(sourceParam string, dest *float64) *ValueBinder {
+	return b.floatValue(sourceParam, dest, 64, false)
+}
+
+// MustFloat64 requires parameter value to exist to be bind to float64 variable. Returns error when value does not exist
+func (b *ValueBinder) MustFloat64(sourceParam string, dest *float64) *ValueBinder {
+	return b.floatValue(sourceParam, dest, 64, true)
+}
+
+// Float32 binds parameter to float32 variable
+func (b *ValueBinder) Float32(sourceParam string, dest *float32) *ValueBinder {
+	return b.floatValue(sourceParam, dest, 32, false)
+}
+
+// MustFloat32 requires parameter value to exist to be bind to float32 variable. Returns error when value does not exist
+func (b *ValueBinder) MustFloat32(sourceParam string, dest *float32) *ValueBinder {
+	return b.floatValue(sourceParam, dest, 32, true)
+}
+
+func (b *ValueBinder) floatValue(sourceParam string, dest interface{}, bitSize int, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+
+	return b.float(sourceParam, value, dest, bitSize)
+}
+
+func (b *ValueBinder) float(sourceParam string, value string, dest interface{}, bitSize int) *ValueBinder {
+	n, err := strconv.ParseFloat(value, bitSize)
+	if err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, fmt.Sprintf("failed to bind field value to float%v", bitSize), err))
+		return b
+	}
+
+	switch d := dest.(type) {
+	case *float64:
+		*d = n
+	case *float32:
+		*d = float32(n)
+	}
+	return b
+}
+
+func (b *ValueBinder) floatsValue(sourceParam string, dest interface{}, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+	return b.floats(sourceParam, values, dest)
+}
+
+func (b *ValueBinder) floats(sourceParam string, values []string, dest interface{}) *ValueBinder {
+	switch d := dest.(type) {
+	case *[]float64:
+		tmp := make([]float64, len(values))
+		for i, v := range values {
+			b.float(sourceParam, v, &tmp[i], 64)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	case *[]float32:
+		tmp := make([]float32, len(values))
+		for i, v := range values {
+			b.float(sourceParam, v, &tmp[i], 32)
+			if b.failFast && b.errors != nil {
+				return b
+			}
+		}
+		if b.errors == nil {
+			*d = tmp
+		}
+	}
+	return b
+}
+
+// Float64s binds parameter values to slice of float64 variables
+func (b *ValueBinder) Float64s(sourceParam string, dest *[]float64) *ValueBinder {
+	return b.floatsValue(sourceParam, dest, false)
+}
+
+// MustFloat64s requires parameter values to exist to be bind to slice of float64 variables. Returns error when values does not exist
+func (b *ValueBinder) MustFloat64s(sourceParam string, dest *[]float64) *ValueBinder {
+	return b.floatsValue(sourceParam, dest, true)
+}
+
+// Float32s binds parameter values to slice of float32 variables
+func (b *ValueBinder) Float32s(sourceParam string, dest *[]float32) *ValueBinder {
+	return b.floatsValue(sourceParam, dest, false)
+}
+
+// MustFloat32s requires parameter values to exist to be bind to slice of float32 variables. Returns error when values does not exist
+func (b *ValueBinder) MustFloat32s(sourceParam string, dest *[]float32) *ValueBinder {
+	return b.floatsValue(sourceParam, dest, true)
+}
+
+// Time binds parameter to time.Time variable
+func (b *ValueBinder) Time(sourceParam string, dest *time.Time, layout string) *ValueBinder {
+	return b.time(sourceParam, dest, layout, false)
+}
+
+// MustTime requires parameter value to exist to be bind to time.Time variable. Returns error when value does not exist
+func (b *ValueBinder) MustTime(sourceParam string, dest *time.Time, layout string) *ValueBinder {
+	return b.time(sourceParam, dest, layout, true)
+}
+
+func (b *ValueBinder) time(sourceParam string, dest *time.Time, layout string, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil))
+		}
+		return b
+	}
+	t, err := time.Parse(layout, value)
+	if err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Time", err))
+		return b
+	}
+	*dest = t
+	return b
+}
+
+// Times binds parameter values to slice of time.Time variables
+func (b *ValueBinder) Times(sourceParam string, dest *[]time.Time, layout string) *ValueBinder {
+	return b.times(sourceParam, dest, layout, false)
+}
+
+// MustTimes requires parameter values to exist to be bind to slice of time.Time variables. Returns error when values does not exist
+func (b *ValueBinder) MustTimes(sourceParam string, dest *[]time.Time, layout string) *ValueBinder {
+	return b.times(sourceParam, dest, layout, true)
+}
+
+func (b *ValueBinder) times(sourceParam string, dest *[]time.Time, layout string, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+
+	tmp := make([]time.Time, len(values))
+	for i, v := range values {
+		t, err := time.Parse(layout, v)
+		if err != nil {
+			b.setError(b.ErrorFunc(sourceParam, []string{v}, "failed to bind field value to Time", err))
+			if b.failFast {
+				return b
+			}
+			continue
+		}
+		tmp[i] = t
+	}
+	if b.errors == nil {
+		*dest = tmp
+	}
+	return b
+}
+
+// Duration binds parameter to time.Duration variable
+func (b *ValueBinder) Duration(sourceParam string, dest *time.Duration) *ValueBinder {
+	return b.duration(sourceParam, dest, false)
+}
+
+// MustDuration requires parameter value to exist to be bind to time.Duration variable. Returns error when value does not exist
+func (b *ValueBinder) MustDuration(sourceParam string, dest *time.Duration) *ValueBinder {
+	return b.duration(sourceParam, dest, true)
+}
+
+func (b *ValueBinder) duration(sourceParam string, dest *time.Duration, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil))
+		}
+		return b
+	}
+	t, err := time.ParseDuration(value)
+	if err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Duration", err))
+		return b
+	}
+	*dest = t
+	return b
+}
+
+// Durations binds parameter values to slice of time.Duration variables
+func (b *ValueBinder) Durations(sourceParam string, dest *[]time.Duration) *ValueBinder {
+	return b.durationsValue(sourceParam, dest, false)
+}
+
+// MustDurations requires parameter values to exist to be bind to slice of time.Duration variables. Returns error when values does not exist
+func (b *ValueBinder) MustDurations(sourceParam string, dest *[]time.Duration) *ValueBinder {
+	return b.durationsValue(sourceParam, dest, true)
+}
+
+func (b *ValueBinder) durationsValue(sourceParam string, dest *[]time.Duration, valueMustExist bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	values := b.ValuesFunc(sourceParam)
+	if len(values) == 0 {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{}, "required field value is empty", nil))
+		}
+		return b
+	}
+	return b.durations(sourceParam, values, dest)
+}
+
+func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]time.Duration) *ValueBinder {
+	tmp := make([]time.Duration, len(values))
+	for i, v := range values {
+		t, err := time.ParseDuration(v)
+		if err != nil {
+			b.setError(b.ErrorFunc(sourceParam, []string{v}, "failed to bind field value to Duration", err))
+			if b.failFast {
+				return b
+			}
+			continue
+		}
+		tmp[i] = t
+	}
+	if b.errors == nil {
+		*dest = tmp
+	}
+	return b
+}
+
+// UnixTime binds parameter to time.Time variable (in local Time corresponding to the given Unix time).
+//
+// Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00
+//
+// Note:
+//  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
+func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder {
+	return b.unixTime(sourceParam, dest, false, false)
+}
+
+// MustUnixTime requires parameter value to exist to be bind to time.Duration variable  (in local Time corresponding
+// to the given Unix time). Returns error when value does not exist.
+//
+// Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00
+//
+// Note:
+//  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
+func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder {
+	return b.unixTime(sourceParam, dest, true, false)
+}
+
+// UnixTimeNano binds parameter to time.Time variable (in local Time corresponding to the given Unix time in nano second precision).
+//
+// Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00
+// Example:          1000000000 binds to 1970-01-01T00:00:01.000000000+00:00
+// Example:           999999999 binds to 1970-01-01T00:00:00.999999999+00:00
+//
+// Note:
+//  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
+//  * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.
+func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {
+	return b.unixTime(sourceParam, dest, false, true)
+}
+
+// MustUnixTimeNano requires parameter value to exist to be bind to time.Duration variable  (in local Time corresponding
+// to the given Unix time value in nano second precision). Returns error when value does not exist.
+//
+// Example: 1609180603123456789 binds to 2020-12-28T18:36:43.123456789+00:00
+// Example:          1000000000 binds to 1970-01-01T00:00:01.000000000+00:00
+// Example:           999999999 binds to 1970-01-01T00:00:00.999999999+00:00
+//
+// Note:
+//  * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
+//  * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.
+func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {
+	return b.unixTime(sourceParam, dest, true, true)
+}
+
+func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExist bool, isNano bool) *ValueBinder {
+	if b.failFast && b.errors != nil {
+		return b
+	}
+
+	value := b.ValueFunc(sourceParam)
+	if value == "" {
+		if valueMustExist {
+			b.setError(b.ErrorFunc(sourceParam, []string{value}, "required field value is empty", nil))
+		}
+		return b
+	}
+
+	n, err := strconv.ParseInt(value, 10, 64)
+	if err != nil {
+		b.setError(b.ErrorFunc(sourceParam, []string{value}, "failed to bind field value to Time", err))
+		return b
+	}
+
+	if isNano {
+		*dest = time.Unix(0, n)
+	} else {
+		*dest = time.Unix(n, 0)
+	}
+	return b
+}
diff --git a/echo.go b/echo.go
index 64e64c2..6db485d 100644
--- a/echo.go
+++ b/echo.go
@@ -503,8 +503,15 @@ func (common) static(prefix, root string, get func(string, HandlerFunc, ...Middl
 		}
 		return c.File(name)
 	}
-	if prefix == "/" {
-		return get(prefix+"*", h)
+	// Handle added routes based on trailing slash:
+	// 	/prefix  => exact route "/prefix" + any route "/prefix/*"
+	// 	/prefix/ => only any route "/prefix/*"
+	if prefix != "" {
+		if prefix[len(prefix)-1] == '/' {
+			// Only add any route for intentional trailing slash
+			return get(prefix+"*", h)
+		}
+		get(prefix, h)
 	}
 	return get(prefix+"/*", h)
 }
diff --git a/middleware/csrf.go b/middleware/csrf.go
index 09a66bb..60f809a 100644
--- a/middleware/csrf.go
+++ b/middleware/csrf.go
@@ -57,6 +57,10 @@ type (
 		// Indicates if CSRF cookie is HTTP only.
 		// Optional. Default value false.
 		CookieHTTPOnly bool `yaml:"cookie_http_only"`
+
+		// Indicates SameSite mode of the CSRF cookie.
+		// Optional. Default value SameSiteDefaultMode.
+		CookieSameSite http.SameSite `yaml:"cookie_same_site"`
 	}
 
 	// csrfTokenExtractor defines a function that takes `echo.Context` and returns
@@ -67,12 +71,13 @@ type (
 var (
 	// DefaultCSRFConfig is the default CSRF middleware config.
 	DefaultCSRFConfig = CSRFConfig{
-		Skipper:      DefaultSkipper,
-		TokenLength:  32,
-		TokenLookup:  "header:" + echo.HeaderXCSRFToken,
-		ContextKey:   "csrf",
-		CookieName:   "_csrf",
-		CookieMaxAge: 86400,
+		Skipper:        DefaultSkipper,
+		TokenLength:    32,
+		TokenLookup:    "header:" + echo.HeaderXCSRFToken,
+		ContextKey:     "csrf",
+		CookieName:     "_csrf",
+		CookieMaxAge:   86400,
+		CookieSameSite: http.SameSiteDefaultMode,
 	}
 )
 
@@ -105,6 +110,9 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 	if config.CookieMaxAge == 0 {
 		config.CookieMaxAge = DefaultCSRFConfig.CookieMaxAge
 	}
+	if config.CookieSameSite == SameSiteNoneMode {
+		config.CookieSecure = true
+	}
 
 	// Initialize
 	parts := strings.Split(config.TokenLookup, ":")
@@ -157,6 +165,9 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
 			if config.CookieDomain != "" {
 				cookie.Domain = config.CookieDomain
 			}
+			if config.CookieSameSite != http.SameSiteDefaultMode {
+				cookie.SameSite = config.CookieSameSite
+			}
 			cookie.Expires = time.Now().Add(time.Duration(config.CookieMaxAge) * time.Second)
 			cookie.Secure = config.CookieSecure
 			cookie.HttpOnly = config.CookieHTTPOnly
diff --git a/middleware/csrf_samesite.go b/middleware/csrf_samesite.go
new file mode 100644
index 0000000..9a27dc4
--- /dev/null
+++ b/middleware/csrf_samesite.go
@@ -0,0 +1,12 @@
+// +build go1.13
+
+package middleware
+
+import (
+	"net/http"
+)
+
+const (
+	// SameSiteNoneMode required to be redefined for Go 1.12 support (see #1524)
+	SameSiteNoneMode http.SameSite = http.SameSiteNoneMode
+)
diff --git a/middleware/csrf_samesite_1.12.go b/middleware/csrf_samesite_1.12.go
new file mode 100644
index 0000000..22076dd
--- /dev/null
+++ b/middleware/csrf_samesite_1.12.go
@@ -0,0 +1,12 @@
+// +build !go1.13
+
+package middleware
+
+import (
+	"net/http"
+)
+
+const (
+	// SameSiteNoneMode required to be redefined for Go 1.12 support (see #1524)
+	SameSiteNoneMode http.SameSite = 4
+)
diff --git a/middleware/timeout.go b/middleware/timeout.go
new file mode 100644
index 0000000..d146541
--- /dev/null
+++ b/middleware/timeout.go
@@ -0,0 +1,81 @@
+// +build go1.13
+
+package middleware
+
+import (
+	"context"
+	"github.com/labstack/echo/v4"
+	"time"
+)
+
+type (
+	// TimeoutConfig defines the config for Timeout middleware.
+	TimeoutConfig struct {
+		// Skipper defines a function to skip middleware.
+		Skipper Skipper
+		// ErrorHandler defines a function which is executed for a timeout
+		// It can be used to define a custom timeout error
+		ErrorHandler TimeoutErrorHandlerWithContext
+		// Timeout configures a timeout for the middleware, defaults to 0 for no timeout
+		Timeout time.Duration
+	}
+
+	// TimeoutErrorHandlerWithContext is an error handler that is used with the timeout middleware so we can
+	// handle the error as we see fit
+	TimeoutErrorHandlerWithContext func(error, echo.Context) error
+)
+
+var (
+	// DefaultTimeoutConfig is the default Timeout middleware config.
+	DefaultTimeoutConfig = TimeoutConfig{
+		Skipper:      DefaultSkipper,
+		Timeout:      0,
+		ErrorHandler: nil,
+	}
+)
+
+// Timeout returns a middleware which recovers from panics anywhere in the chain
+// and handles the control to the centralized HTTPErrorHandler.
+func Timeout() echo.MiddlewareFunc {
+	return TimeoutWithConfig(DefaultTimeoutConfig)
+}
+
+// TimeoutWithConfig returns a Timeout middleware with config.
+// See: `Timeout()`.
+func TimeoutWithConfig(config TimeoutConfig) echo.MiddlewareFunc {
+	// Defaults
+	if config.Skipper == nil {
+		config.Skipper = DefaultTimeoutConfig.Skipper
+	}
+
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if config.Skipper(c) || config.Timeout == 0 {
+				return next(c)
+			}
+
+			ctx, cancel := context.WithTimeout(c.Request().Context(), config.Timeout)
+			defer cancel()
+
+			// this does a deep clone of the context, wondering if there is a better way to do this?
+			c.SetRequest(c.Request().Clone(ctx))
+
+			done := make(chan error, 1)
+			go func() {
+				// This goroutine will keep running even if this middleware times out and
+				// will be stopped when ctx.Done() is called down the next(c) call chain
+				done <- next(c)
+			}()
+
+			select {
+			case <-ctx.Done():
+				if config.ErrorHandler != nil {
+					return config.ErrorHandler(ctx.Err(), c)
+				}
+				return ctx.Err()
+			case err := <-done:
+				return err
+			}
+		}
+	}
+}
diff --git a/router.go b/router.go
index 749dbf4..5010659 100644
--- a/router.go
+++ b/router.go
@@ -372,14 +372,14 @@ func (r *Router) Find(method, path string, c Context) {
 			if search == "" && (nn == nil || cn.parent == nil || cn.ppath != "") {
 				break
 			}
+			// Handle special case of trailing slash route with existing any route (see #1526)
+			if search == "" && path[len(path)-1] == '/' && cn.anyChildren != nil {
+				goto Any
+			}
 		}
 
 		// Attempt to go back up the tree on no matching prefix or no remaining search
 		if l != pl || search == "" {
-			// Handle special case of trailing slash route with existing any route (see #1526)
-			if path[len(path)-1] == '/' && cn.anyChildren != nil {
-				goto Any
-			}
 			if nn == nil { // Issue #1348
 				return // Not found
 			}


Test patch:
diff --git a/bind_test.go b/bind_test.go
index 345fbdf..e8868b3 100644
--- a/bind_test.go
+++ b/bind_test.go
@@ -160,6 +160,31 @@ var values = map[string][]string{
 	"ST":      {"bar"},
 }
 
+func TestToMultipleFields(t *testing.T) {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, "/?id=1&ID=2", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	type Root struct {
+		ID     int64 `query:"id"`
+		Child2 struct {
+			ID int64
+		}
+		Child1 struct {
+			ID int64 `query:"id"`
+		}
+	}
+
+	u := new(Root)
+	err := c.Bind(u)
+	if assert.NoError(t, err) {
+		assert.Equal(t, int64(1), u.ID)        // perfectly reasonable
+		assert.Equal(t, int64(1), u.Child1.ID) // untagged struct containing tagged field gets filled (by tag)
+		assert.Equal(t, int64(0), u.Child2.ID) // untagged struct containing untagged field should not be bind
+	}
+}
+
 func TestBindJSON(t *testing.T) {
 	assert := assert.New(t)
 	testBindOkay(assert, strings.NewReader(userJSON), MIMEApplicationJSON)
@@ -238,10 +263,13 @@ func TestBindUnmarshalParam(t *testing.T) {
 	rec := httptest.NewRecorder()
 	c := e.NewContext(req, rec)
 	result := struct {
-		T  Timestamp   `query:"ts"`
-		TA []Timestamp `query:"ta"`
-		SA StringArray `query:"sa"`
-		ST Struct
+		T         Timestamp   `query:"ts"`
+		TA        []Timestamp `query:"ta"`
+		SA        StringArray `query:"sa"`
+		ST        Struct
+		StWithTag struct {
+			Foo string `query:"st"`
+		}
 	}{}
 	err := c.Bind(&result)
 	ts := Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC))
@@ -252,7 +280,8 @@ func TestBindUnmarshalParam(t *testing.T) {
 		assert.Equal(ts, result.T)
 		assert.Equal(StringArray([]string{"one", "two", "three"}), result.SA)
 		assert.Equal([]Timestamp{ts, ts}, result.TA)
-		assert.Equal(Struct{"baz"}, result.ST)
+		assert.Equal(Struct{""}, result.ST)       // child struct does not have a field with matching tag
+		assert.Equal("baz", result.StWithTag.Foo) // child struct has field with matching tag
 	}
 }
 
@@ -274,7 +303,7 @@ func TestBindUnmarshalText(t *testing.T) {
 		assert.Equal(t, ts, result.T)
 		assert.Equal(t, StringArray([]string{"one", "two", "three"}), result.SA)
 		assert.Equal(t, []time.Time{ts, ts}, result.TA)
-		assert.Equal(t, Struct{"baz"}, result.ST)
+		assert.Equal(t, Struct{""}, result.ST) // field in child struct does not have tag
 	}
 }
 
@@ -323,11 +352,27 @@ func TestBindUnsupportedMediaType(t *testing.T) {
 }
 
 func TestBindbindData(t *testing.T) {
-	assert := assert.New(t)
+	a := assert.New(t)
 	ts := new(bindTestStruct)
 	b := new(DefaultBinder)
-	b.bindData(ts, values, "form")
-	assertBindTestStruct(assert, ts)
+	err := b.bindData(ts, values, "form")
+	a.NoError(err)
+
+	a.Equal(0, ts.I)
+	a.Equal(int8(0), ts.I8)
+	a.Equal(int16(0), ts.I16)
+	a.Equal(int32(0), ts.I32)
+	a.Equal(int64(0), ts.I64)
+	a.Equal(uint(0), ts.UI)
+	a.Equal(uint8(0), ts.UI8)
+	a.Equal(uint16(0), ts.UI16)
+	a.Equal(uint32(0), ts.UI32)
+	a.Equal(uint64(0), ts.UI64)
+	a.Equal(false, ts.B)
+	a.Equal(float32(0), ts.F32)
+	a.Equal(float64(0), ts.F64)
+	a.Equal("", ts.S)
+	a.Equal("", ts.cantSet)
 }
 
 func TestBindParam(t *testing.T) {
@@ -470,20 +515,6 @@ func TestBindSetFields(t *testing.T) {
 	}
 }
 
-func BenchmarkBindbindData(b *testing.B) {
-	b.ReportAllocs()
-	assert := assert.New(b)
-	ts := new(bindTestStruct)
-	binder := new(DefaultBinder)
-	var err error
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		err = binder.bindData(ts, values, "form")
-	}
-	assert.NoError(err)
-	assertBindTestStruct(assert, ts)
-}
-
 func BenchmarkBindbindDataWithTags(b *testing.B) {
 	b.ReportAllocs()
 	assert := assert.New(b)
@@ -560,8 +591,9 @@ func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
 	// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
 
 	type Opts struct {
-		ID   int    `json:"id"`
-		Node string `json:"node"`
+		ID   int    `json:"id" form:"id" query:"id"`
+		Node string `json:"node" form:"node" query:"node" param:"node"`
+		Lang string
 	}
 
 	var testCases = []struct {
@@ -727,8 +759,8 @@ func TestDefaultBinder_BindBody(t *testing.T) {
 	// 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"`
+		ID   int    `json:"id" xml:"id" form:"id" query:"id"`
+		Node string `json:"node" xml:"node" form:"node" query:"node" param:"node"`
 	}
 	type Nodes struct {
 		Nodes []Node `xml:"node" form:"node"`
@@ -824,7 +856,7 @@ func TestDefaultBinder_BindBody(t *testing.T) {
 			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",
+			name:             "ok, FORM POST bind to struct with: path + query + body",
 			givenURL:         "/api/real_node/endpoint?node=xxx",
 			givenMethod:      http.MethodPost,
 			givenContentType: MIMEApplicationForm,
diff --git a/binder_external_test.go b/binder_external_test.go
new file mode 100644
index 0000000..f1aecb5
--- /dev/null
+++ b/binder_external_test.go
@@ -0,0 +1,130 @@
+// run tests as external package to get real feel for API
+package echo_test
+
+import (
+	"encoding/base64"
+	"fmt"
+	"github.com/labstack/echo/v4"
+	"log"
+	"net/http"
+	"net/http/httptest"
+)
+
+func ExampleValueBinder_BindErrors() {
+	// example route function that binds query params to different destinations and returns all bind errors in one go
+	routeFunc := func(c echo.Context) error {
+		var opts struct {
+			Active bool
+			IDs    []int64
+		}
+		length := int64(50) // default length is 50
+
+		b := echo.QueryParamsBinder(c)
+
+		errs := b.Int64("length", &length).
+			Int64s("ids", &opts.IDs).
+			Bool("active", &opts.Active).
+			BindErrors() // returns all errors
+		if errs != nil {
+			for _, err := range errs {
+				bErr := err.(*echo.BindingError)
+				log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values)
+			}
+			return fmt.Errorf("%v fields failed to bind", len(errs))
+		}
+		fmt.Printf("active = %v, length = %v, ids = %v", opts.Active, length, opts.IDs)
+
+		return c.JSON(http.StatusOK, opts)
+	}
+
+	e := echo.New()
+	c := e.NewContext(
+		httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil),
+		httptest.NewRecorder(),
+	)
+
+	_ = routeFunc(c)
+
+	// Output: active = true, length = 25, ids = [1 2 3]
+}
+
+func ExampleValueBinder_BindError() {
+	// example route function that binds query params to different destinations and stops binding on first bind error
+	failFastRouteFunc := func(c echo.Context) error {
+		var opts struct {
+			Active bool
+			IDs    []int64
+		}
+		length := int64(50) // default length is 50
+
+		// create binder that stops binding at first error
+		b := echo.QueryParamsBinder(c)
+
+		err := b.Int64("length", &length).
+			Int64s("ids", &opts.IDs).
+			Bool("active", &opts.Active).
+			BindError() // returns first binding error
+		if err != nil {
+			bErr := err.(*echo.BindingError)
+			return fmt.Errorf("my own custom error for field: %s values: %v", bErr.Field, bErr.Values)
+		}
+		fmt.Printf("active = %v, length = %v, ids = %v\n", opts.Active, length, opts.IDs)
+
+		return c.JSON(http.StatusOK, opts)
+	}
+
+	e := echo.New()
+	c := e.NewContext(
+		httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil),
+		httptest.NewRecorder(),
+	)
+
+	_ = failFastRouteFunc(c)
+
+	// Output: active = true, length = 25, ids = [1 2 3]
+}
+
+func ExampleValueBinder_CustomFunc() {
+	// example route function that binds query params using custom function closure
+	routeFunc := func(c echo.Context) error {
+		length := int64(50) // default length is 50
+		var binary []byte
+
+		b := echo.QueryParamsBinder(c)
+		errs := b.Int64("length", &length).
+			CustomFunc("base64", func(values []string) []error {
+				if len(values) == 0 {
+					return nil
+				}
+				decoded, err := base64.URLEncoding.DecodeString(values[0])
+				if err != nil {
+					// in this example we use only first param value but url could contain multiple params in reality and
+					// therefore in theory produce multiple binding errors
+					return []error{echo.NewBindingError("base64", values[0:1], "failed to decode base64", err)}
+				}
+				binary = decoded
+				return nil
+			}).
+			BindErrors() // returns all errors
+
+		if errs != nil {
+			for _, err := range errs {
+				bErr := err.(*echo.BindingError)
+				log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values)
+			}
+			return fmt.Errorf("%v fields failed to bind", len(errs))
+		}
+		fmt.Printf("length = %v, base64 = %s", length, binary)
+
+		return c.JSON(http.StatusOK, "ok")
+	}
+
+	e := echo.New()
+	c := e.NewContext(
+		httptest.NewRequest(http.MethodGet, "/api/endpoint?length=25&base64=SGVsbG8gV29ybGQ%3D", nil),
+		httptest.NewRecorder(),
+	)
+	_ = routeFunc(c)
+
+	// Output: length = 25, base64 = Hello World
+}
diff --git a/binder_go1.15_test.go b/binder_go1.15_test.go
new file mode 100644
index 0000000..018628c
--- /dev/null
+++ b/binder_go1.15_test.go
@@ -0,0 +1,265 @@
+// +build go1.15
+
+package echo
+
+/**
+	Since version 1.15 time.Time and time.Duration error message pattern has changed (values are wrapped now in \"\")
+	So pre 1.15 these tests fail with similar error:
+
+  expected: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param"
+  actual  : "code=400, message=failed to bind field value to Duration, internal=time: invalid duration nope, field=param"
+*/
+
+import (
+	"errors"
+	"github.com/stretchr/testify/assert"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+)
+
+func createTestContext15(URL string, body io.Reader, pathParams map[string]string) Context {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, URL, body)
+	if body != nil {
+		req.Header.Set(HeaderContentType, MIMEApplicationJSON)
+	}
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	if len(pathParams) > 0 {
+		names := make([]string, 0)
+		values := make([]string, 0)
+		for name, value := range pathParams {
+			names = append(names, name)
+			values = append(values, value)
+		}
+		c.SetParamNames(names...)
+		c.SetParamValues(values...)
+	}
+
+	return c
+}
+
+func TestValueBinder_TimeError(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		whenLayout      string
+		expectValue     time.Time
+		expectError     string
+	}{
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: time.Time{},
+			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: time.Time{},
+			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext15(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := time.Time{}
+			var err error
+			if tc.whenMust {
+				err = b.MustTime("param", &dest, tc.whenLayout).BindError()
+			} else {
+				err = b.Time("param", &dest, tc.whenLayout).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_TimesError(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		whenLayout      string
+		expectValue     []time.Time
+		expectError     string
+	}{
+		{
+			name:          "nok, fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []time.Time(nil),
+			expectError:   "code=400, message=failed to bind field value to Time, internal=parsing time \"1\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"1\" as \"2006\", field=param",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []time.Time(nil),
+			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []time.Time(nil),
+			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext15(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			layout := time.RFC3339
+			if tc.whenLayout != "" {
+				layout = tc.whenLayout
+			}
+
+			var dest []time.Time
+			var err error
+			if tc.whenMust {
+				err = b.MustTimes("param", &dest, layout).BindError()
+			} else {
+				err = b.Times("param", &dest, layout).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_DurationError(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     time.Duration
+		expectError     string
+	}{
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 0,
+			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 0,
+			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext15(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			var dest time.Duration
+			var err error
+			if tc.whenMust {
+				err = b.MustDuration("param", &dest).BindError()
+			} else {
+				err = b.Duration("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_DurationsError(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []time.Duration
+		expectError     string
+	}{
+		{
+			name:          "nok, fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []time.Duration(nil),
+			expectError:   "code=400, message=failed to bind field value to Duration, internal=time: missing unit in duration \"1\", field=param",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []time.Duration(nil),
+			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []time.Duration(nil),
+			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext15(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			var dest []time.Duration
+			var err error
+			if tc.whenMust {
+				err = b.MustDurations("param", &dest).BindError()
+			} else {
+				err = b.Durations("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/binder_test.go b/binder_test.go
new file mode 100644
index 0000000..946906a
--- /dev/null
+++ b/binder_test.go
@@ -0,0 +1,2757 @@
+// run tests as external package to get real feel for API
+package echo
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+)
+
+func createTestContext(URL string, body io.Reader, pathParams map[string]string) Context {
+	e := New()
+	req := httptest.NewRequest(http.MethodGet, URL, body)
+	if body != nil {
+		req.Header.Set(HeaderContentType, MIMEApplicationJSON)
+	}
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	if len(pathParams) > 0 {
+		names := make([]string, 0)
+		values := make([]string, 0)
+		for name, value := range pathParams {
+			names = append(names, name)
+			values = append(values, value)
+		}
+		c.SetParamNames(names...)
+		c.SetParamValues(values...)
+	}
+
+	return c
+}
+
+func TestBindingError_Error(t *testing.T) {
+	err := NewBindingError("id", []string{"1", "nope"}, "bind failed", errors.New("internal error"))
+	assert.EqualError(t, err, `code=400, message=bind failed, internal=internal error, field=id`)
+
+	bErr := err.(*BindingError)
+	assert.Equal(t, 400, bErr.Code)
+	assert.Equal(t, "bind failed", bErr.Message)
+	assert.Equal(t, errors.New("internal error"), bErr.Internal)
+
+	assert.Equal(t, "id", bErr.Field)
+	assert.Equal(t, []string{"1", "nope"}, bErr.Values)
+}
+
+func TestBindingError_ErrorJSON(t *testing.T) {
+	err := NewBindingError("id", []string{"1", "nope"}, "bind failed", errors.New("internal error"))
+
+	resp, err := json.Marshal(err)
+
+	assert.Equal(t, `{"field":"id","message":"bind failed"}`, string(resp))
+}
+
+func TestPathParamsBinder(t *testing.T) {
+	c := createTestContext("/api/user/999", nil, map[string]string{
+		"id":    "1",
+		"nr":    "2",
+		"slice": "3",
+	})
+	b := PathParamsBinder(c)
+
+	id := int64(99)
+	nr := int64(88)
+	var slice = make([]int64, 0)
+	var notExisting = make([]int64, 0)
+	err := b.Int64("id", &id).
+		Int64("nr", &nr).
+		Int64s("slice", &slice).
+		Int64s("not_existing", &notExisting).
+		BindError()
+
+	assert.NoError(t, err)
+	assert.Equal(t, int64(1), id)
+	assert.Equal(t, int64(2), nr)
+	assert.Equal(t, []int64{3}, slice)      // binding params to slice does not make sense but it should not panic either
+	assert.Equal(t, []int64{}, notExisting) // binding params to slice does not make sense but it should not panic either
+}
+
+func TestQueryParamsBinder_FailFast(t *testing.T) {
+	var testCases = []struct {
+		name          string
+		whenURL       string
+		givenFailFast bool
+		expectError   []string
+	}{
+		{
+			name:          "ok, FailFast=true stops at first error",
+			whenURL:       "/api/user/999?nr=en&id=nope",
+			givenFailFast: true,
+			expectError: []string{
+				`code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=id`,
+			},
+		},
+		{
+			name:          "ok, FailFast=false encounters all errors",
+			whenURL:       "/api/user/999?nr=en&id=nope",
+			givenFailFast: false,
+			expectError: []string{
+				`code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=id`,
+				`code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing "en": invalid syntax, field=nr`,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, map[string]string{"id": "999"})
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			id := int64(99)
+			nr := int64(88)
+			errs := b.Int64("id", &id).
+				Int64("nr", &nr).
+				BindErrors()
+
+			assert.Len(t, errs, len(tc.expectError))
+			for _, err := range errs {
+				assert.Contains(t, tc.expectError, err.Error())
+			}
+		})
+	}
+}
+
+func TestFormFieldBinder(t *testing.T) {
+	e := New()
+	body := `texta=foo&slice=5`
+	req := httptest.NewRequest(http.MethodPost, "/api/search?id=1&nr=2&slice=3&slice=4", strings.NewReader(body))
+	req.Header.Set(HeaderContentLength, strconv.Itoa(len(body)))
+	req.Header.Set(HeaderContentType, MIMEApplicationForm)
+
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	b := FormFieldBinder(c)
+
+	var texta string
+	id := int64(99)
+	nr := int64(88)
+	var slice = make([]int64, 0)
+	var notExisting = make([]int64, 0)
+	err := b.
+		Int64s("slice", &slice).
+		Int64("id", &id).
+		Int64("nr", &nr).
+		String("texta", &texta).
+		Int64s("notExisting", &notExisting).
+		BindError()
+
+	assert.NoError(t, err)
+	assert.Equal(t, "foo", texta)
+	assert.Equal(t, int64(1), id)
+	assert.Equal(t, int64(2), nr)
+	assert.Equal(t, []int64{5, 3, 4}, slice)
+	assert.Equal(t, []int64{}, notExisting)
+}
+
+func TestValueBinder_errorStopsBinding(t *testing.T) {
+	// this test documents "feature" that binding multiple params can change destination if it was binded before
+	// failing parameter binding
+
+	c := createTestContext("/api/user/999?id=1&nr=nope", nil, nil)
+	b := QueryParamsBinder(c)
+
+	id := int64(99) // will be changed before nr binding fails
+	nr := int64(88) // will not be changed
+	err := b.Int64("id", &id).
+		Int64("nr", &nr).
+		BindError()
+
+	assert.EqualError(t, err, "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=nr")
+	assert.Equal(t, int64(1), id)
+	assert.Equal(t, int64(88), nr)
+}
+
+func TestValueBinder_BindError(t *testing.T) {
+	c := createTestContext("/api/user/999?nr=en&id=nope", nil, nil)
+	b := QueryParamsBinder(c)
+
+	id := int64(99)
+	nr := int64(88)
+	err := b.Int64("id", &id).
+		Int64("nr", &nr).
+		BindError()
+
+	assert.EqualError(t, err, "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=id")
+	assert.Nil(t, b.errors)
+	assert.Nil(t, b.BindError())
+}
+
+func TestValueBinder_GetValues(t *testing.T) {
+	var testCases = []struct {
+		name           string
+		whenValuesFunc func(sourceParam string) []string
+		expect         []int64
+		expectError    string
+	}{
+		{
+			name:   "ok, default implementation",
+			expect: []int64{1, 101},
+		},
+		{
+			name: "ok, values returns nil",
+			whenValuesFunc: func(sourceParam string) []string {
+				return nil
+			},
+			expect: []int64(nil),
+		},
+		{
+			name: "ok, values returns empty slice",
+			whenValuesFunc: func(sourceParam string) []string {
+				return []string{}
+			},
+			expect: []int64(nil),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext("/search?nr=en&id=1&id=101", nil, nil)
+			b := QueryParamsBinder(c)
+			if tc.whenValuesFunc != nil {
+				b.ValuesFunc = tc.whenValuesFunc
+			}
+
+			var IDs []int64
+			err := b.Int64s("id", &IDs).BindError()
+
+			assert.Equal(t, tc.expect, IDs)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_CustomFuncWithError(t *testing.T) {
+	c := createTestContext("/search?nr=en&id=1&id=101", nil, nil)
+	b := QueryParamsBinder(c)
+
+	id := int64(99)
+	givenCustomFunc := func(values []string) []error {
+		assert.Equal(t, []string{"1", "101"}, values)
+
+		return []error{
+			errors.New("first error"),
+			errors.New("second error"),
+		}
+	}
+	err := b.CustomFunc("id", givenCustomFunc).BindError()
+
+	assert.Equal(t, int64(99), id)
+	assert.EqualError(t, err, "first error")
+}
+
+func TestValueBinder_CustomFunc(t *testing.T) {
+	var testCases = []struct {
+		name              string
+		givenFailFast     bool
+		givenFuncErrors   []error
+		whenURL           string
+		expectParamValues []string
+		expectValue       interface{}
+		expectErrors      []string
+	}{
+		{
+			name:              "ok, binds value",
+			whenURL:           "/search?nr=en&id=1&id=100",
+			expectParamValues: []string{"1", "100"},
+			expectValue:       int64(1000),
+		},
+		{
+			name:              "ok, params values empty, value is not changed",
+			whenURL:           "/search?nr=en",
+			expectParamValues: []string{},
+			expectValue:       int64(99),
+		},
+		{
+			name:              "nok, previous errors fail fast without binding value",
+			givenFailFast:     true,
+			whenURL:           "/search?nr=en&id=1&id=100",
+			expectParamValues: []string{"1", "100"},
+			expectValue:       int64(99),
+			expectErrors:      []string{"previous error"},
+		},
+		{
+			name: "nok, func returns errors",
+			givenFuncErrors: []error{
+				errors.New("first error"),
+				errors.New("second error"),
+			},
+			whenURL:           "/search?nr=en&id=1&id=100",
+			expectParamValues: []string{"1", "100"},
+			expectValue:       int64(99),
+			expectErrors:      []string{"first error", "second error"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			id := int64(99)
+			givenCustomFunc := func(values []string) []error {
+				assert.Equal(t, tc.expectParamValues, values)
+				if tc.givenFuncErrors == nil {
+					id = 1000 // emulated conversion and setting value
+					return nil
+				}
+				return tc.givenFuncErrors
+			}
+			errs := b.CustomFunc("id", givenCustomFunc).BindErrors()
+
+			assert.Equal(t, tc.expectValue, id)
+			if tc.expectErrors != nil {
+				assert.Len(t, errs, len(tc.expectErrors))
+				for _, err := range errs {
+					assert.Contains(t, tc.expectErrors, err.Error())
+				}
+			} else {
+				assert.Nil(t, errs)
+			}
+		})
+	}
+}
+
+func TestValueBinder_MustCustomFunc(t *testing.T) {
+	var testCases = []struct {
+		name              string
+		givenFailFast     bool
+		givenFuncErrors   []error
+		whenURL           string
+		expectParamValues []string
+		expectValue       interface{}
+		expectErrors      []string
+	}{
+		{
+			name:              "ok, binds value",
+			whenURL:           "/search?nr=en&id=1&id=100",
+			expectParamValues: []string{"1", "100"},
+			expectValue:       int64(1000),
+		},
+		{
+			name:              "nok, params values empty, returns error, value is not changed",
+			whenURL:           "/search?nr=en",
+			expectParamValues: []string{},
+			expectValue:       int64(99),
+			expectErrors:      []string{"code=400, message=required field value is empty, field=id"},
+		},
+		{
+			name:              "nok, previous errors fail fast without binding value",
+			givenFailFast:     true,
+			whenURL:           "/search?nr=en&id=1&id=100",
+			expectParamValues: []string{"1", "100"},
+			expectValue:       int64(99),
+			expectErrors:      []string{"previous error"},
+		},
+		{
+			name: "nok, func returns errors",
+			givenFuncErrors: []error{
+				errors.New("first error"),
+				errors.New("second error"),
+			},
+			whenURL:           "/search?nr=en&id=1&id=100",
+			expectParamValues: []string{"1", "100"},
+			expectValue:       int64(99),
+			expectErrors:      []string{"first error", "second error"},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			id := int64(99)
+			givenCustomFunc := func(values []string) []error {
+				assert.Equal(t, tc.expectParamValues, values)
+				if tc.givenFuncErrors == nil {
+					id = 1000 // emulated conversion and setting value
+					return nil
+				}
+				return tc.givenFuncErrors
+			}
+			errs := b.MustCustomFunc("id", givenCustomFunc).BindErrors()
+
+			assert.Equal(t, tc.expectValue, id)
+			if tc.expectErrors != nil {
+				assert.Len(t, errs, len(tc.expectErrors))
+				for _, err := range errs {
+					assert.Contains(t, tc.expectErrors, err.Error())
+				}
+			} else {
+				assert.Nil(t, errs)
+			}
+		})
+	}
+}
+
+func TestValueBinder_String(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     string
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=en&param=de",
+			expectValue: "en",
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nr=en",
+			expectValue: "default",
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?nr=en&id=1&id=100",
+			expectValue:   "default",
+			expectError:   "previous error",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=en&param=de",
+			expectValue: "en",
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nr=en",
+			expectValue: "default",
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?nr=en&id=1&id=100",
+			expectValue:   "default",
+			expectError:   "previous error",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := "default"
+			var err error
+			if tc.whenMust {
+				err = b.MustString("param", &dest).BindError()
+			} else {
+				err = b.String("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Strings(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []string
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=en&param=de",
+			expectValue: []string{"en", "de"},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nr=en",
+			expectValue: []string{"default"},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?nr=en&id=1&id=100",
+			expectValue:   []string{"default"},
+			expectError:   "previous error",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=en&param=de",
+			expectValue: []string{"en", "de"},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nr=en",
+			expectValue: []string{"default"},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?nr=en&id=1&id=100",
+			expectValue:   []string{"default"},
+			expectError:   "previous error",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := []string{"default"}
+			var err error
+			if tc.whenMust {
+				err = b.MustStrings("param", &dest).BindError()
+			} else {
+				err = b.Strings("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Int64_intValue(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     int64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=1&param=100",
+			expectValue: 1,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: 99,
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   99,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 99,
+			expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1&param=100",
+			expectValue: 1,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: 99,
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   99,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 99,
+			expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := int64(99)
+			var err error
+			if tc.whenMust {
+				err = b.MustInt64("param", &dest).BindError()
+			} else {
+				err = b.Int64("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Int_errorMessage(t *testing.T) {
+	// int/uint (without byte size) has a little bit different error message so test these separately
+	c := createTestContext("/search?param=nope", nil, nil)
+	b := QueryParamsBinder(c).FailFast(false)
+
+	destInt := 99
+	destUint := uint(98)
+	errs := b.Int("param", &destInt).Uint("param", &destUint).BindErrors()
+
+	assert.Equal(t, 99, destInt)
+	assert.Equal(t, uint(98), destUint)
+	assert.EqualError(t, errs[0], `code=400, message=failed to bind field value to int, internal=strconv.ParseInt: parsing "nope": invalid syntax, field=param`)
+	assert.EqualError(t, errs[1], `code=400, message=failed to bind field value to uint, internal=strconv.ParseUint: parsing "nope": invalid syntax, field=param`)
+}
+
+func TestValueBinder_Uint64_uintValue(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     uint64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=1&param=100",
+			expectValue: 1,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: 99,
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   99,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 99,
+			expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1&param=100",
+			expectValue: 1,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: 99,
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   99,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 99,
+			expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := uint64(99)
+			var err error
+			if tc.whenMust {
+				err = b.MustUint64("param", &dest).BindError()
+			} else {
+				err = b.Uint64("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Int_Types(t *testing.T) {
+	type target struct {
+		int64      int64
+		mustInt64  int64
+		uint64     uint64
+		mustUint64 uint64
+
+		int32      int32
+		mustInt32  int32
+		uint32     uint32
+		mustUint32 uint32
+
+		int16      int16
+		mustInt16  int16
+		uint16     uint16
+		mustUint16 uint16
+
+		int8      int8
+		mustInt8  int8
+		uint8     uint8
+		mustUint8 uint8
+
+		byte     byte
+		mustByte byte
+
+		int      int
+		mustInt  int
+		uint     uint
+		mustUint uint
+	}
+	types := []string{
+		"int64=1",
+		"mustInt64=2",
+		"uint64=3",
+		"mustUint64=4",
+
+		"int32=5",
+		"mustInt32=6",
+		"uint32=7",
+		"mustUint32=8",
+
+		"int16=9",
+		"mustInt16=10",
+		"uint16=11",
+		"mustUint16=12",
+
+		"int8=13",
+		"mustInt8=14",
+		"uint8=15",
+		"mustUint8=16",
+
+		"byte=17",
+		"mustByte=18",
+
+		"int=19",
+		"mustInt=20",
+		"uint=21",
+		"mustUint=22",
+	}
+	c := createTestContext("/search?"+strings.Join(types, "&"), nil, nil)
+	b := QueryParamsBinder(c)
+
+	dest := target{}
+	err := b.
+		Int64("int64", &dest.int64).
+		MustInt64("mustInt64", &dest.mustInt64).
+		Uint64("uint64", &dest.uint64).
+		MustUint64("mustUint64", &dest.mustUint64).
+		Int32("int32", &dest.int32).
+		MustInt32("mustInt32", &dest.mustInt32).
+		Uint32("uint32", &dest.uint32).
+		MustUint32("mustUint32", &dest.mustUint32).
+		Int16("int16", &dest.int16).
+		MustInt16("mustInt16", &dest.mustInt16).
+		Uint16("uint16", &dest.uint16).
+		MustUint16("mustUint16", &dest.mustUint16).
+		Int8("int8", &dest.int8).
+		MustInt8("mustInt8", &dest.mustInt8).
+		Uint8("uint8", &dest.uint8).
+		MustUint8("mustUint8", &dest.mustUint8).
+		Byte("byte", &dest.byte).
+		MustByte("mustByte", &dest.mustByte).
+		Int("int", &dest.int).
+		MustInt("mustInt", &dest.mustInt).
+		Uint("uint", &dest.uint).
+		MustUint("mustUint", &dest.mustUint).
+		BindError()
+
+	assert.NoError(t, err)
+	assert.Equal(t, int64(1), dest.int64)
+	assert.Equal(t, int64(2), dest.mustInt64)
+	assert.Equal(t, uint64(3), dest.uint64)
+	assert.Equal(t, uint64(4), dest.mustUint64)
+
+	assert.Equal(t, int32(5), dest.int32)
+	assert.Equal(t, int32(6), dest.mustInt32)
+	assert.Equal(t, uint32(7), dest.uint32)
+	assert.Equal(t, uint32(8), dest.mustUint32)
+
+	assert.Equal(t, int16(9), dest.int16)
+	assert.Equal(t, int16(10), dest.mustInt16)
+	assert.Equal(t, uint16(11), dest.uint16)
+	assert.Equal(t, uint16(12), dest.mustUint16)
+
+	assert.Equal(t, int8(13), dest.int8)
+	assert.Equal(t, int8(14), dest.mustInt8)
+	assert.Equal(t, uint8(15), dest.uint8)
+	assert.Equal(t, uint8(16), dest.mustUint8)
+
+	assert.Equal(t, uint8(17), dest.byte)
+	assert.Equal(t, uint8(18), dest.mustByte)
+
+	assert.Equal(t, 19, dest.int)
+	assert.Equal(t, 20, dest.mustInt)
+	assert.Equal(t, uint(21), dest.uint)
+	assert.Equal(t, uint(22), dest.mustUint)
+}
+
+func TestValueBinder_Int64s_intsValue(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []int64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=1&param=2&param=1",
+			expectValue: []int64{1, 2, 1},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []int64{99},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []int64{99},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []int64{99},
+			expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1&param=2&param=1",
+			expectValue: []int64{1, 2, 1},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []int64{99},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []int64{99},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []int64{99},
+			expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := []int64{99} // when values are set with bind - contents before bind is gone
+			var err error
+			if tc.whenMust {
+				err = b.MustInt64s("param", &dest).BindError()
+			} else {
+				err = b.Int64s("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Uint64s_uintsValue(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []uint64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=1&param=2&param=1",
+			expectValue: []uint64{1, 2, 1},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []uint64{99},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []uint64{99},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []uint64{99},
+			expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1&param=2&param=1",
+			expectValue: []uint64{1, 2, 1},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []uint64{99},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []uint64{99},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []uint64{99},
+			expectError: "code=400, message=failed to bind field value to uint64, internal=strconv.ParseUint: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := []uint64{99} // when values are set with bind - contents before bind is gone
+			var err error
+			if tc.whenMust {
+				err = b.MustUint64s("param", &dest).BindError()
+			} else {
+				err = b.Uint64s("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Ints_Types(t *testing.T) {
+	type target struct {
+		int64      []int64
+		mustInt64  []int64
+		uint64     []uint64
+		mustUint64 []uint64
+
+		int32      []int32
+		mustInt32  []int32
+		uint32     []uint32
+		mustUint32 []uint32
+
+		int16      []int16
+		mustInt16  []int16
+		uint16     []uint16
+		mustUint16 []uint16
+
+		int8      []int8
+		mustInt8  []int8
+		uint8     []uint8
+		mustUint8 []uint8
+
+		int      []int
+		mustInt  []int
+		uint     []uint
+		mustUint []uint
+	}
+	types := []string{
+		"int64=1",
+		"mustInt64=2",
+		"uint64=3",
+		"mustUint64=4",
+
+		"int32=5",
+		"mustInt32=6",
+		"uint32=7",
+		"mustUint32=8",
+
+		"int16=9",
+		"mustInt16=10",
+		"uint16=11",
+		"mustUint16=12",
+
+		"int8=13",
+		"mustInt8=14",
+		"uint8=15",
+		"mustUint8=16",
+
+		"int=19",
+		"mustInt=20",
+		"uint=21",
+		"mustUint=22",
+	}
+	url := "/search?"
+	for _, v := range types {
+		url = url + "&" + v + "&" + v
+	}
+	c := createTestContext(url, nil, nil)
+	b := QueryParamsBinder(c)
+
+	dest := target{}
+	err := b.
+		Int64s("int64", &dest.int64).
+		MustInt64s("mustInt64", &dest.mustInt64).
+		Uint64s("uint64", &dest.uint64).
+		MustUint64s("mustUint64", &dest.mustUint64).
+		Int32s("int32", &dest.int32).
+		MustInt32s("mustInt32", &dest.mustInt32).
+		Uint32s("uint32", &dest.uint32).
+		MustUint32s("mustUint32", &dest.mustUint32).
+		Int16s("int16", &dest.int16).
+		MustInt16s("mustInt16", &dest.mustInt16).
+		Uint16s("uint16", &dest.uint16).
+		MustUint16s("mustUint16", &dest.mustUint16).
+		Int8s("int8", &dest.int8).
+		MustInt8s("mustInt8", &dest.mustInt8).
+		Uint8s("uint8", &dest.uint8).
+		MustUint8s("mustUint8", &dest.mustUint8).
+		Ints("int", &dest.int).
+		MustInts("mustInt", &dest.mustInt).
+		Uints("uint", &dest.uint).
+		MustUints("mustUint", &dest.mustUint).
+		BindError()
+
+	assert.NoError(t, err)
+	assert.Equal(t, []int64{1, 1}, dest.int64)
+	assert.Equal(t, []int64{2, 2}, dest.mustInt64)
+	assert.Equal(t, []uint64{3, 3}, dest.uint64)
+	assert.Equal(t, []uint64{4, 4}, dest.mustUint64)
+
+	assert.Equal(t, []int32{5, 5}, dest.int32)
+	assert.Equal(t, []int32{6, 6}, dest.mustInt32)
+	assert.Equal(t, []uint32{7, 7}, dest.uint32)
+	assert.Equal(t, []uint32{8, 8}, dest.mustUint32)
+
+	assert.Equal(t, []int16{9, 9}, dest.int16)
+	assert.Equal(t, []int16{10, 10}, dest.mustInt16)
+	assert.Equal(t, []uint16{11, 11}, dest.uint16)
+	assert.Equal(t, []uint16{12, 12}, dest.mustUint16)
+
+	assert.Equal(t, []int8{13, 13}, dest.int8)
+	assert.Equal(t, []int8{14, 14}, dest.mustInt8)
+	assert.Equal(t, []uint8{15, 15}, dest.uint8)
+	assert.Equal(t, []uint8{16, 16}, dest.mustUint8)
+
+	assert.Equal(t, []int{19, 19}, dest.int)
+	assert.Equal(t, []int{20, 20}, dest.mustInt)
+	assert.Equal(t, []uint{21, 21}, dest.uint)
+	assert.Equal(t, []uint{22, 22}, dest.mustUint)
+}
+
+func TestValueBinder_Ints_Types_FailFast(t *testing.T) {
+	// FailFast() should stop parsing and return early
+	errTmpl := "code=400, message=failed to bind field value to %v, internal=strconv.Parse%v: parsing \"nope\": invalid syntax, field=param"
+	c := createTestContext("/search?param=1&param=nope&param=2", nil, nil)
+
+	var dest64 []int64
+	err := QueryParamsBinder(c).FailFast(true).Int64s("param", &dest64).BindError()
+	assert.Equal(t, []int64(nil), dest64)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int64", "Int"))
+
+	var dest32 []int32
+	err = QueryParamsBinder(c).FailFast(true).Int32s("param", &dest32).BindError()
+	assert.Equal(t, []int32(nil), dest32)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int32", "Int"))
+
+	var dest16 []int16
+	err = QueryParamsBinder(c).FailFast(true).Int16s("param", &dest16).BindError()
+	assert.Equal(t, []int16(nil), dest16)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int16", "Int"))
+
+	var dest8 []int8
+	err = QueryParamsBinder(c).FailFast(true).Int8s("param", &dest8).BindError()
+	assert.Equal(t, []int8(nil), dest8)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int8", "Int"))
+
+	var dest []int
+	err = QueryParamsBinder(c).FailFast(true).Ints("param", &dest).BindError()
+	assert.Equal(t, []int(nil), dest)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "int", "Int"))
+
+	var destu64 []uint64
+	err = QueryParamsBinder(c).FailFast(true).Uint64s("param", &destu64).BindError()
+	assert.Equal(t, []uint64(nil), destu64)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint64", "Uint"))
+
+	var destu32 []uint32
+	err = QueryParamsBinder(c).FailFast(true).Uint32s("param", &destu32).BindError()
+	assert.Equal(t, []uint32(nil), destu32)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint32", "Uint"))
+
+	var destu16 []uint16
+	err = QueryParamsBinder(c).FailFast(true).Uint16s("param", &destu16).BindError()
+	assert.Equal(t, []uint16(nil), destu16)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint16", "Uint"))
+
+	var destu8 []uint8
+	err = QueryParamsBinder(c).FailFast(true).Uint8s("param", &destu8).BindError()
+	assert.Equal(t, []uint8(nil), destu8)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint8", "Uint"))
+
+	var destu []uint
+	err = QueryParamsBinder(c).FailFast(true).Uints("param", &destu).BindError()
+	assert.Equal(t, []uint(nil), destu)
+	assert.EqualError(t, err, fmt.Sprintf(errTmpl, "uint", "Uint"))
+}
+
+func TestValueBinder_Bool(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     bool
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=true&param=1",
+			expectValue: true,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: false,
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   false,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: false,
+			expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1&param=100",
+			expectValue: true,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: false,
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   false,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: false,
+			expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := false
+			var err error
+			if tc.whenMust {
+				err = b.MustBool("param", &dest).BindError()
+			} else {
+				err = b.Bool("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Bools(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []bool
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=true&param=false&param=1&param=0",
+			expectValue: []bool{true, false, true, false},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []bool(nil),
+		},
+		{
+			name:            "nok, previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []bool(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=true&param=nope&param=100",
+			expectValue: []bool(nil),
+			expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:          "nok, conversion fails fast, value is not changed",
+			givenFailFast: true,
+			whenURL:       "/search?param=true&param=nope&param=100",
+			expectValue:   []bool(nil),
+			expectError:   "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=true&param=false&param=1&param=0",
+			expectValue: []bool{true, false, true, false},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []bool(nil),
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:            "nok (must), previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenMust:        true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []bool(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []bool(nil),
+			expectError: "code=400, message=failed to bind field value to bool, internal=strconv.ParseBool: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			var dest []bool
+			var err error
+			if tc.whenMust {
+				err = b.MustBools("param", &dest).BindError()
+			} else {
+				err = b.Bools("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Float64(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     float64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=4.3&param=1",
+			expectValue: 4.3,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: 1.123,
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   1.123,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 1.123,
+			expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=4.3&param=100",
+			expectValue: 4.3,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: 1.123,
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   1.123,
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 1.123,
+			expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := 1.123
+			var err error
+			if tc.whenMust {
+				err = b.MustFloat64("param", &dest).BindError()
+			} else {
+				err = b.Float64("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Float64s(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []float64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=4.3&param=0",
+			expectValue: []float64{4.3, 0},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []float64(nil),
+		},
+		{
+			name:            "nok, previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []float64(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []float64(nil),
+			expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:          "nok, conversion fails fast, value is not changed",
+			givenFailFast: true,
+			whenURL:       "/search?param=0&param=nope&param=100",
+			expectValue:   []float64(nil),
+			expectError:   "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=4.3&param=0",
+			expectValue: []float64{4.3, 0},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []float64(nil),
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:            "nok (must), previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenMust:        true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []float64(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []float64(nil),
+			expectError: "code=400, message=failed to bind field value to float64, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			var dest []float64
+			var err error
+			if tc.whenMust {
+				err = b.MustFloat64s("param", &dest).BindError()
+			} else {
+				err = b.Float64s("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Float32(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenNoFailFast bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     float32
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=4.3&param=1",
+			expectValue: 4.3,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: 1.123,
+		},
+		{
+			name:            "nok, previous errors fail fast without binding value",
+			givenNoFailFast: true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     1.123,
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 1.123,
+			expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=4.3&param=100",
+			expectValue: 4.3,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: 1.123,
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:            "nok (must), previous errors fail fast without binding value",
+			givenNoFailFast: true,
+			whenMust:        true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     1.123,
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: 1.123,
+			expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenNoFailFast)
+			if tc.givenNoFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := float32(1.123)
+			var err error
+			if tc.whenMust {
+				err = b.MustFloat32("param", &dest).BindError()
+			} else {
+				err = b.Float32("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Float32s(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []float32
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=4.3&param=0",
+			expectValue: []float32{4.3, 0},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []float32(nil),
+		},
+		{
+			name:            "nok, previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []float32(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []float32(nil),
+			expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:          "nok, conversion fails fast, value is not changed",
+			givenFailFast: true,
+			whenURL:       "/search?param=0&param=nope&param=100",
+			expectValue:   []float32(nil),
+			expectError:   "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=4.3&param=0",
+			expectValue: []float32{4.3, 0},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []float32(nil),
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:            "nok (must), previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenMust:        true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []float32(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []float32(nil),
+			expectError: "code=400, message=failed to bind field value to float32, internal=strconv.ParseFloat: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			var dest []float32
+			var err error
+			if tc.whenMust {
+				err = b.MustFloat32s("param", &dest).BindError()
+			} else {
+				err = b.Float32s("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Time(t *testing.T) {
+	exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00")
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		whenLayout      string
+		expectValue     time.Time
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00",
+			whenLayout:  time.RFC3339,
+			expectValue: exampleTime,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: time.Time{},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   time.Time{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00",
+			whenLayout:  time.RFC3339,
+			expectValue: exampleTime,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: time.Time{},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   time.Time{},
+			expectError:   "previous error",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := time.Time{}
+			var err error
+			if tc.whenMust {
+				err = b.MustTime("param", &dest, tc.whenLayout).BindError()
+			} else {
+				err = b.Time("param", &dest, tc.whenLayout).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Times(t *testing.T) {
+	exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00")
+	exampleTime2, _ := time.Parse(time.RFC3339, "2000-01-02T09:45:31+00:00")
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		whenLayout      string
+		expectValue     []time.Time
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00",
+			whenLayout:  time.RFC3339,
+			expectValue: []time.Time{exampleTime, exampleTime2},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []time.Time(nil),
+		},
+		{
+			name:            "nok, previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []time.Time(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00",
+			whenLayout:  time.RFC3339,
+			expectValue: []time.Time{exampleTime, exampleTime2},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []time.Time(nil),
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:            "nok (must), previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenMust:        true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []time.Time(nil),
+			expectError:     "previous error",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			layout := time.RFC3339
+			if tc.whenLayout != "" {
+				layout = tc.whenLayout
+			}
+
+			var dest []time.Time
+			var err error
+			if tc.whenMust {
+				err = b.MustTimes("param", &dest, layout).BindError()
+			} else {
+				err = b.Times("param", &dest, layout).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Duration(t *testing.T) {
+	example := 42 * time.Second
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     time.Duration
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=42s&param=1ms",
+			expectValue: example,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: 0,
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   0,
+			expectError:   "previous error",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=42s&param=1ms",
+			expectValue: example,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: 0,
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   0,
+			expectError:   "previous error",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			var dest time.Duration
+			var err error
+			if tc.whenMust {
+				err = b.MustDuration("param", &dest).BindError()
+			} else {
+				err = b.Duration("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_Durations(t *testing.T) {
+	exampleDuration := 42 * time.Second
+	exampleDuration2 := 1 * time.Millisecond
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []time.Duration
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=42s&param=1ms",
+			expectValue: []time.Duration{exampleDuration, exampleDuration2},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []time.Duration(nil),
+		},
+		{
+			name:            "nok, previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []time.Duration(nil),
+			expectError:     "previous error",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=42s&param=1ms",
+			expectValue: []time.Duration{exampleDuration, exampleDuration2},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []time.Duration(nil),
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:            "nok (must), previous errors fail fast without binding value",
+			givenFailFast:   true,
+			givenBindErrors: []error{errors.New("previous error")},
+			whenMust:        true,
+			whenURL:         "/search?param=1&param=100",
+			expectValue:     []time.Duration(nil),
+			expectError:     "previous error",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			b.errors = tc.givenBindErrors
+
+			var dest []time.Duration
+			var err error
+			if tc.whenMust {
+				err = b.MustDurations("param", &dest).BindError()
+			} else {
+				err = b.Durations("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_BindUnmarshaler(t *testing.T) {
+	exampleTime, _ := time.Parse(time.RFC3339, "2020-12-23T09:45:31+02:00")
+
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     Timestamp
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00",
+			expectValue: Timestamp(exampleTime),
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: Timestamp{},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   Timestamp{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: Timestamp{},
+			expectError: "code=400, message=failed to bind field value to BindUnmarshaler interface, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=2020-12-23T09:45:31%2B02:00&param=2000-01-02T09:45:31%2B00:00",
+			expectValue: Timestamp(exampleTime),
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: Timestamp{},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   Timestamp{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: Timestamp{},
+			expectError: "code=400, message=failed to bind field value to BindUnmarshaler interface, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			var dest Timestamp
+			var err error
+			if tc.whenMust {
+				err = b.MustBindUnmarshaler("param", &dest).BindError()
+			} else {
+				err = b.BindUnmarshaler("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_BindWithDelimiter_types(t *testing.T) {
+	var testCases = []struct {
+		name    string
+		whenURL string
+		expect  interface{}
+	}{
+		{
+			name:   "ok, strings",
+			expect: []string{"1", "2", "1"},
+		},
+		{
+			name:   "ok, int64",
+			expect: []int64{1, 2, 1},
+		},
+		{
+			name:   "ok, int32",
+			expect: []int32{1, 2, 1},
+		},
+		{
+			name:   "ok, int16",
+			expect: []int16{1, 2, 1},
+		},
+		{
+			name:   "ok, int8",
+			expect: []int8{1, 2, 1},
+		},
+		{
+			name:   "ok, int",
+			expect: []int{1, 2, 1},
+		},
+		{
+			name:   "ok, uint64",
+			expect: []uint64{1, 2, 1},
+		},
+		{
+			name:   "ok, uint32",
+			expect: []uint32{1, 2, 1},
+		},
+		{
+			name:   "ok, uint16",
+			expect: []uint16{1, 2, 1},
+		},
+		{
+			name:   "ok, uint8",
+			expect: []uint8{1, 2, 1},
+		},
+		{
+			name:   "ok, uint",
+			expect: []uint{1, 2, 1},
+		},
+		{
+			name:   "ok, float64",
+			expect: []float64{1, 2, 1},
+		},
+		{
+			name:   "ok, float32",
+			expect: []float32{1, 2, 1},
+		},
+		{
+			name:    "ok, bool",
+			whenURL: "/search?param=1,false&param=true",
+			expect:  []bool{true, false, true},
+		},
+		{
+			name:    "ok, Duration",
+			whenURL: "/search?param=1s,42s&param=1ms",
+			expect:  []time.Duration{1 * time.Second, 42 * time.Second, 1 * time.Millisecond},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			URL := "/search?param=1,2&param=1"
+			if tc.whenURL != "" {
+				URL = tc.whenURL
+			}
+			c := createTestContext(URL, nil, nil)
+			b := QueryParamsBinder(c)
+
+			switch tc.expect.(type) {
+			case []string:
+				var dest []string
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []int64:
+				var dest []int64
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []int32:
+				var dest []int32
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []int16:
+				var dest []int16
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []int8:
+				var dest []int8
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []int:
+				var dest []int
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []uint64:
+				var dest []uint64
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []uint32:
+				var dest []uint32
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []uint16:
+				var dest []uint16
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []uint8:
+				var dest []uint8
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []uint:
+				var dest []uint
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []float64:
+				var dest []float64
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []float32:
+				var dest []float32
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []bool:
+				var dest []bool
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			case []time.Duration:
+				var dest []time.Duration
+				assert.NoError(t, b.BindWithDelimiter("param", &dest, ",").BindError())
+				assert.Equal(t, tc.expect, dest)
+			default:
+				assert.Fail(t, "invalid type")
+			}
+		})
+	}
+}
+
+func TestValueBinder_BindWithDelimiter(t *testing.T) {
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     []int64
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value",
+			whenURL:     "/search?param=1,2&param=1",
+			expectValue: []int64{1, 2, 1},
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: []int64(nil),
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []int64(nil),
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []int64(nil),
+			expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1,2&param=1",
+			expectValue: []int64{1, 2, 1},
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: []int64(nil),
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   []int64(nil),
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: []int64(nil),
+			expectError: "code=400, message=failed to bind field value to int64, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			var dest []int64
+			var err error
+			if tc.whenMust {
+				err = b.MustBindWithDelimiter("param", &dest, ",").BindError()
+			} else {
+				err = b.BindWithDelimiter("param", &dest, ",").BindError()
+			}
+
+			assert.Equal(t, tc.expectValue, dest)
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestBindWithDelimiter_invalidType(t *testing.T) {
+	c := createTestContext("/search?param=1&param=100", nil, nil)
+	b := QueryParamsBinder(c)
+
+	var dest []BindUnmarshaler
+	err := b.BindWithDelimiter("param", &dest, ",").BindError()
+	assert.Equal(t, []BindUnmarshaler(nil), dest)
+	assert.EqualError(t, err, "code=400, message=unsupported bind type, field=param")
+}
+
+func TestValueBinder_UnixTime(t *testing.T) {
+	exampleTime, _ := time.Parse(time.RFC3339, "2020-12-28T18:36:43+00:00") // => 1609180603
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     time.Time
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value, unix time in seconds",
+			whenURL:     "/search?param=1609180603&param=1609180604",
+			expectValue: exampleTime,
+		},
+		{
+			name:        "ok, binds value, unix time over int32 value",
+			whenURL:     "/search?param=2147483648&param=1609180604",
+			expectValue: time.Unix(2147483648, 0),
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: time.Time{},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   time.Time{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: time.Time{},
+			expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1609180603&param=1609180604",
+			expectValue: exampleTime,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: time.Time{},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   time.Time{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: time.Time{},
+			expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := time.Time{}
+			var err error
+			if tc.whenMust {
+				err = b.MustUnixTime("param", &dest).BindError()
+			} else {
+				err = b.UnixTime("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano())
+			assert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC))
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestValueBinder_UnixTimeNano(t *testing.T) {
+	exampleTime, _ := time.Parse(time.RFC3339, "2020-12-28T18:36:43.000000000+00:00")         // => 1609180603
+	exampleTimeNano, _ := time.Parse(time.RFC3339Nano, "2020-12-28T18:36:43.123456789+00:00") // => 1609180603123456789
+	exampleTimeNanoBelowSec, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00.999999999+00:00")
+	var testCases = []struct {
+		name            string
+		givenFailFast   bool
+		givenBindErrors []error
+		whenURL         string
+		whenMust        bool
+		expectValue     time.Time
+		expectError     string
+	}{
+		{
+			name:        "ok, binds value, unix time in nano seconds (sec precision)",
+			whenURL:     "/search?param=1609180603000000000&param=1609180604",
+			expectValue: exampleTime,
+		},
+		{
+			name:        "ok, binds value, unix time in nano seconds",
+			whenURL:     "/search?param=1609180603123456789&param=1609180604",
+			expectValue: exampleTimeNano,
+		},
+		{
+			name:        "ok, binds value, unix time in nano seconds (below 1 sec)",
+			whenURL:     "/search?param=999999999&param=1609180604",
+			expectValue: exampleTimeNanoBelowSec,
+		},
+		{
+			name:        "ok, params values empty, value is not changed",
+			whenURL:     "/search?nope=1",
+			expectValue: time.Time{},
+		},
+		{
+			name:          "nok, previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   time.Time{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok, conversion fails, value is not changed",
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: time.Time{},
+			expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+		{
+			name:        "ok (must), binds value",
+			whenMust:    true,
+			whenURL:     "/search?param=1609180603000000000&param=1609180604",
+			expectValue: exampleTime,
+		},
+		{
+			name:        "ok (must), params values empty, returns error, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?nope=1",
+			expectValue: time.Time{},
+			expectError: "code=400, message=required field value is empty, field=param",
+		},
+		{
+			name:          "nok (must), previous errors fail fast without binding value",
+			givenFailFast: true,
+			whenMust:      true,
+			whenURL:       "/search?param=1&param=100",
+			expectValue:   time.Time{},
+			expectError:   "previous error",
+		},
+		{
+			name:        "nok (must), conversion fails, value is not changed",
+			whenMust:    true,
+			whenURL:     "/search?param=nope&param=100",
+			expectValue: time.Time{},
+			expectError: "code=400, message=failed to bind field value to Time, internal=strconv.ParseInt: parsing \"nope\": invalid syntax, field=param",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			c := createTestContext(tc.whenURL, nil, nil)
+			b := QueryParamsBinder(c).FailFast(tc.givenFailFast)
+			if tc.givenFailFast {
+				b.errors = []error{errors.New("previous error")}
+			}
+
+			dest := time.Time{}
+			var err error
+			if tc.whenMust {
+				err = b.MustUnixTimeNano("param", &dest).BindError()
+			} else {
+				err = b.UnixTimeNano("param", &dest).BindError()
+			}
+
+			assert.Equal(t, tc.expectValue.UnixNano(), dest.UnixNano())
+			assert.Equal(t, tc.expectValue.In(time.UTC), dest.In(time.UTC))
+			if tc.expectError != "" {
+				assert.EqualError(t, err, tc.expectError)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func BenchmarkDefaultBinder_BindInt64_single(b *testing.B) {
+	type Opts struct {
+		Param int64 `query:"param"`
+	}
+	c := createTestContext("/search?param=1&param=100", nil, nil)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	binder := new(DefaultBinder)
+	for i := 0; i < b.N; i++ {
+		var dest Opts
+		_ = binder.Bind(&dest, c)
+	}
+}
+
+func BenchmarkValueBinder_BindInt64_single(b *testing.B) {
+	c := createTestContext("/search?param=1&param=100", nil, nil)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	type Opts struct {
+		Param int64
+	}
+	binder := QueryParamsBinder(c)
+	for i := 0; i < b.N; i++ {
+		var dest Opts
+		_ = binder.Int64("param", &dest.Param).BindError()
+	}
+}
+
+func BenchmarkRawFunc_Int64_single(b *testing.B) {
+	c := createTestContext("/search?param=1&param=100", nil, nil)
+
+	rawFunc := func(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
+	}
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	type Opts struct {
+		Param int64
+	}
+	for i := 0; i < b.N; i++ {
+		var dest Opts
+		if n, ok := rawFunc(c.QueryParam("param"), 1); ok {
+			dest.Param = n
+		}
+	}
+}
+
+func BenchmarkDefaultBinder_BindInt64_10_fields(b *testing.B) {
+	type Opts struct {
+		Int64  int64  `query:"int64"`
+		Int32  int32  `query:"int32"`
+		Int16  int16  `query:"int16"`
+		Int8   int8   `query:"int8"`
+		String string `query:"string"`
+
+		Uint64  uint64   `query:"uint64"`
+		Uint32  uint32   `query:"uint32"`
+		Uint16  uint16   `query:"uint16"`
+		Uint8   uint8    `query:"uint8"`
+		Strings []string `query:"strings"`
+	}
+	c := createTestContext("/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second", nil, nil)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	binder := new(DefaultBinder)
+	for i := 0; i < b.N; i++ {
+		var dest Opts
+		_ = binder.Bind(&dest, c)
+		if dest.Int64 != 1 {
+			b.Fatalf("int64!=1")
+		}
+	}
+}
+
+func BenchmarkValueBinder_BindInt64_10_fields(b *testing.B) {
+	type Opts struct {
+		Int64  int64  `query:"int64"`
+		Int32  int32  `query:"int32"`
+		Int16  int16  `query:"int16"`
+		Int8   int8   `query:"int8"`
+		String string `query:"string"`
+
+		Uint64  uint64   `query:"uint64"`
+		Uint32  uint32   `query:"uint32"`
+		Uint16  uint16   `query:"uint16"`
+		Uint8   uint8    `query:"uint8"`
+		Strings []string `query:"strings"`
+	}
+	c := createTestContext("/search?int64=1&int32=2&int16=3&int8=4&string=test&uint64=5&uint32=6&uint16=7&uint8=8&strings=first&strings=second", nil, nil)
+
+	b.ReportAllocs()
+	b.ResetTimer()
+	binder := QueryParamsBinder(c)
+	for i := 0; i < b.N; i++ {
+		var dest Opts
+		_ = binder.
+			Int64("int64", &dest.Int64).
+			Int32("int32", &dest.Int32).
+			Int16("int16", &dest.Int16).
+			Int8("int8", &dest.Int8).
+			String("string", &dest.String).
+			Uint64("int64", &dest.Uint64).
+			Uint32("int32", &dest.Uint32).
+			Uint16("int16", &dest.Uint16).
+			Uint8("int8", &dest.Uint8).
+			Strings("strings", &dest.Strings).
+			BindError()
+		if dest.Int64 != 1 {
+			b.Fatalf("int64!=1")
+		}
+	}
+}
diff --git a/echo_test.go b/echo_test.go
index 7f35974..781b901 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -105,6 +105,32 @@ func TestEchoStatic(t *testing.T) {
 			expectHeaderLocation: "/folder/",
 			expectBodyStartsWith: "",
 		},
+		{
+			name:                 "Directory Redirect with non-root path",
+			givenPrefix:          "/static",
+			givenRoot:            "_fixture",
+			whenURL:              "/static",
+			expectStatus:         http.StatusMovedPermanently,
+			expectHeaderLocation: "/static/",
+			expectBodyStartsWith: "",
+		},
+		{
+			name:                 "Prefixed directory 404 (request URL without slash)",
+			givenPrefix:          "/folder/", // trailing slash will intentionally not match "/folder"
+			givenRoot:            "_fixture",
+			whenURL:              "/folder", // no trailing slash
+			expectStatus:         http.StatusNotFound,
+			expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
+		},
+		{
+			name:                 "Prefixed directory redirect (without slash redirect to slash)",
+			givenPrefix:          "/folder", // no trailing slash shall match /folder and /folder/*
+			givenRoot:            "_fixture",
+			whenURL:              "/folder", // no trailing slash
+			expectStatus:         http.StatusMovedPermanently,
+			expectHeaderLocation: "/folder/",
+			expectBodyStartsWith: "",
+		},
 		{
 			name:                 "Directory with index.html",
 			givenPrefix:          "/",
@@ -113,6 +139,22 @@ func TestEchoStatic(t *testing.T) {
 			expectStatus:         http.StatusOK,
 			expectBodyStartsWith: "<!doctype html>",
 		},
+		{
+			name:                 "Prefixed directory with index.html (prefix ending with slash)",
+			givenPrefix:          "/assets/",
+			givenRoot:            "_fixture",
+			whenURL:              "/assets/",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: "<!doctype html>",
+		},
+		{
+			name:                 "Prefixed directory with index.html (prefix ending without slash)",
+			givenPrefix:          "/assets",
+			givenRoot:            "_fixture",
+			whenURL:              "/assets/",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: "<!doctype html>",
+		},
 		{
 			name:                 "Sub-directory with index.html",
 			givenPrefix:          "/",
@@ -164,6 +206,40 @@ func TestEchoStatic(t *testing.T) {
 	}
 }
 
+func TestEchoStaticRedirectIndex(t *testing.T) {
+	assert := assert.New(t)
+	e := New()
+
+	// HandlerFunc
+	e.Static("/static", "_fixture")
+
+	errCh := make(chan error)
+
+	go func() {
+		errCh <- e.Start("127.0.0.1:1323")
+	}()
+
+	time.Sleep(200 * time.Millisecond)
+
+	if resp, err := http.Get("http://127.0.0.1:1323/static"); err == nil {
+		defer resp.Body.Close()
+		assert.Equal(http.StatusOK, resp.StatusCode)
+
+		if body, err := ioutil.ReadAll(resp.Body); err == nil {
+			assert.Equal(true, strings.HasPrefix(string(body), "<!doctype html>"))
+		} else {
+			assert.Fail(err.Error())
+		}
+
+	} else {
+		assert.Fail(err.Error())
+	}
+
+	if err := e.Close(); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestEchoFile(t *testing.T) {
 	e := New()
 	e.File("/walle", "_fixture/images/walle.png")
diff --git a/middleware/csrf_samesite_test.go b/middleware/csrf_samesite_test.go
new file mode 100644
index 0000000..26c5bc4
--- /dev/null
+++ b/middleware/csrf_samesite_test.go
@@ -0,0 +1,33 @@
+// +build go1.13
+
+package middleware
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/assert"
+)
+
+// Test for SameSiteModeNone moved to separate file for Go 1.12 support
+func TestCSRFWithSameSiteModeNone(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: SameSiteNoneMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.Regexp(t, "SameSite=None", rec.Header()["Set-Cookie"])
+	assert.Regexp(t, "Secure", rec.Header()["Set-Cookie"])
+}
diff --git a/middleware/csrf_test.go b/middleware/csrf_test.go
index efb4dd1..ebe4dbc 100644
--- a/middleware/csrf_test.go
+++ b/middleware/csrf_test.go
@@ -1,6 +1,7 @@
 package middleware
 
 import (
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -81,3 +82,59 @@ func TestCSRFTokenFromQuery(t *testing.T) {
 	assert.Error(t, err)
 	csrfTokenFromQuery("csrf")
 }
+
+func TestCSRFSetSameSiteMode(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: http.SameSiteStrictMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.Regexp(t, "SameSite=Strict", rec.Header()["Set-Cookie"])
+}
+
+func TestCSRFWithoutSameSiteMode(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"])
+}
+
+func TestCSRFWithSameSiteDefaultMode(t *testing.T) {
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	csrf := CSRFWithConfig(CSRFConfig{
+		CookieSameSite: http.SameSiteDefaultMode,
+	})
+
+	h := csrf(func(c echo.Context) error {
+		return c.String(http.StatusOK, "test")
+	})
+
+	r := h(c)
+	assert.NoError(t, r)
+	fmt.Println(rec.Header()["Set-Cookie"])
+	assert.NotRegexp(t, "SameSite=", rec.Header()["Set-Cookie"])
+}
diff --git a/middleware/static_test.go b/middleware/static_test.go
index 3e6ca56..8c0c97d 100644
--- a/middleware/static_test.go
+++ b/middleware/static_test.go
@@ -1,11 +1,13 @@
 package middleware
 
 import (
-	"github.com/labstack/echo/v4"
-	"github.com/stretchr/testify/assert"
 	"net/http"
 	"net/http/httptest"
+	"strings"
 	"testing"
+
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestStatic(t *testing.T) {
@@ -131,3 +133,147 @@ func TestStatic(t *testing.T) {
 		})
 	}
 }
+
+func TestStatic_GroupWithStatic(t *testing.T) {
+	var testCases = []struct {
+		name                 string
+		givenGroup           string
+		givenPrefix          string
+		givenRoot            string
+		whenURL              string
+		expectStatus         int
+		expectHeaderLocation string
+		expectBodyStartsWith string
+	}{
+		{
+			name:                 "ok",
+			givenPrefix:          "/images",
+			givenRoot:            "../_fixture/images",
+			whenURL:              "/group/images/walle.png",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
+		},
+		{
+			name:                 "No file",
+			givenPrefix:          "/images",
+			givenRoot:            "../_fixture/scripts",
+			whenURL:              "/group/images/bolt.png",
+			expectStatus:         http.StatusNotFound,
+			expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
+		},
+		{
+			name:                 "Directory not found (no trailing slash)",
+			givenPrefix:          "/images",
+			givenRoot:            "../_fixture/images",
+			whenURL:              "/group/images/",
+			expectStatus:         http.StatusNotFound,
+			expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
+		},
+		{
+			name:                 "Directory redirect",
+			givenPrefix:          "/",
+			givenRoot:            "../_fixture",
+			whenURL:              "/group/folder",
+			expectStatus:         http.StatusMovedPermanently,
+			expectHeaderLocation: "/group/folder/",
+			expectBodyStartsWith: "",
+		},
+		{
+			name:                 "Prefixed directory 404 (request URL without slash)",
+			givenGroup:           "_fixture",
+			givenPrefix:          "/folder/", // trailing slash will intentionally not match "/folder"
+			givenRoot:            "../_fixture",
+			whenURL:              "/_fixture/folder", // no trailing slash
+			expectStatus:         http.StatusNotFound,
+			expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
+		},
+		{
+			name:                 "Prefixed directory redirect (without slash redirect to slash)",
+			givenGroup:           "_fixture",
+			givenPrefix:          "/folder", // no trailing slash shall match /folder and /folder/*
+			givenRoot:            "../_fixture",
+			whenURL:              "/_fixture/folder", // no trailing slash
+			expectStatus:         http.StatusMovedPermanently,
+			expectHeaderLocation: "/_fixture/folder/",
+			expectBodyStartsWith: "",
+		},
+		{
+			name:                 "Directory with index.html",
+			givenPrefix:          "/",
+			givenRoot:            "../_fixture",
+			whenURL:              "/group/",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: "<!doctype html>",
+		},
+		{
+			name:                 "Prefixed directory with index.html (prefix ending with slash)",
+			givenPrefix:          "/assets/",
+			givenRoot:            "../_fixture",
+			whenURL:              "/group/assets/",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: "<!doctype html>",
+		},
+		{
+			name:                 "Prefixed directory with index.html (prefix ending without slash)",
+			givenPrefix:          "/assets",
+			givenRoot:            "../_fixture",
+			whenURL:              "/group/assets/",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: "<!doctype html>",
+		},
+		{
+			name:                 "Sub-directory with index.html",
+			givenPrefix:          "/",
+			givenRoot:            "../_fixture",
+			whenURL:              "/group/folder/",
+			expectStatus:         http.StatusOK,
+			expectBodyStartsWith: "<!doctype html>",
+		},
+		{
+			name:                 "do not allow directory traversal (backslash - windows separator)",
+			givenPrefix:          "/",
+			givenRoot:            "../_fixture/",
+			whenURL:              `/group/..\\middleware/basic_auth.go`,
+			expectStatus:         http.StatusNotFound,
+			expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
+		},
+		{
+			name:                 "do not allow directory traversal (slash - unix separator)",
+			givenPrefix:          "/",
+			givenRoot:            "../_fixture/",
+			whenURL:              `/group/../middleware/basic_auth.go`,
+			expectStatus:         http.StatusNotFound,
+			expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			e := echo.New()
+			group := "/group"
+			if tc.givenGroup != "" {
+				group = tc.givenGroup
+			}
+			g := e.Group(group)
+			g.Static(tc.givenPrefix, tc.givenRoot)
+
+			req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
+			rec := httptest.NewRecorder()
+			e.ServeHTTP(rec, req)
+			assert.Equal(t, tc.expectStatus, rec.Code)
+			body := rec.Body.String()
+			if tc.expectBodyStartsWith != "" {
+				assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
+			} else {
+				assert.Equal(t, "", body)
+			}
+
+			if tc.expectHeaderLocation != "" {
+				assert.Equal(t, tc.expectHeaderLocation, rec.Header().Get(echo.HeaderLocation))
+			} else {
+				_, ok := rec.Result().Header[echo.HeaderLocation]
+				assert.False(t, ok)
+			}
+		})
+	}
+}
diff --git a/middleware/timeout_test.go b/middleware/timeout_test.go
new file mode 100644
index 0000000..c0e9459
--- /dev/null
+++ b/middleware/timeout_test.go
@@ -0,0 +1,177 @@
+// +build go1.13
+
+package middleware
+
+import (
+	"context"
+	"errors"
+	"github.com/labstack/echo/v4"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"strings"
+	"testing"
+	"time"
+)
+
+func TestTimeoutSkipper(t *testing.T) {
+	t.Parallel()
+	m := TimeoutWithConfig(TimeoutConfig{
+		Skipper: func(context echo.Context) bool {
+			return true
+		},
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		assert.NotEqual(t, "*context.timerCtx", reflect.TypeOf(c.Request().Context()).String())
+		return nil
+	})(c)
+
+	assert.NoError(t, err)
+}
+
+func TestTimeoutWithTimeout0(t *testing.T) {
+	t.Parallel()
+	m := TimeoutWithConfig(TimeoutConfig{
+		Timeout: 0,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		assert.NotEqual(t, "*context.timerCtx", reflect.TypeOf(c.Request().Context()).String())
+		return nil
+	})(c)
+
+	assert.NoError(t, err)
+}
+
+func TestTimeoutIsCancelable(t *testing.T) {
+	t.Parallel()
+	m := TimeoutWithConfig(TimeoutConfig{
+		Timeout: time.Minute,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		assert.EqualValues(t, "*context.timerCtx", reflect.TypeOf(c.Request().Context()).String())
+		return nil
+	})(c)
+
+	assert.NoError(t, err)
+}
+
+func TestTimeoutErrorOutInHandler(t *testing.T) {
+	t.Parallel()
+	m := Timeout()
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		return errors.New("err")
+	})(c)
+
+	assert.Error(t, err)
+}
+
+func TestTimeoutTimesOutAfterPredefinedTimeoutWithErrorHandler(t *testing.T) {
+	t.Parallel()
+	m := TimeoutWithConfig(TimeoutConfig{
+		Timeout: time.Second,
+		ErrorHandler: func(err error, e echo.Context) error {
+			assert.EqualError(t, err, context.DeadlineExceeded.Error())
+			return errors.New("err")
+		},
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		time.Sleep(time.Minute)
+		return nil
+	})(c)
+
+	assert.EqualError(t, err, errors.New("err").Error())
+}
+
+func TestTimeoutTimesOutAfterPredefinedTimeout(t *testing.T) {
+	t.Parallel()
+	m := TimeoutWithConfig(TimeoutConfig{
+		Timeout: time.Second,
+	})
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
+	rec := httptest.NewRecorder()
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		time.Sleep(time.Minute)
+		return nil
+	})(c)
+
+	assert.EqualError(t, err, context.DeadlineExceeded.Error())
+}
+
+func TestTimeoutTestRequestClone(t *testing.T) {
+	t.Parallel()
+	req := httptest.NewRequest(http.MethodPost, "/uri?query=value", strings.NewReader(url.Values{"form": {"value"}}.Encode()))
+	req.AddCookie(&http.Cookie{Name: "cookie", Value: "value"})
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	rec := httptest.NewRecorder()
+
+	m := TimeoutWithConfig(TimeoutConfig{
+		// Timeout has to be defined or the whole flow for timeout middleware will be skipped
+		Timeout: time.Second,
+	})
+
+	e := echo.New()
+	c := e.NewContext(req, rec)
+
+	err := m(func(c echo.Context) error {
+		// Cookie test
+		cookie, err := c.Request().Cookie("cookie")
+		if assert.NoError(t, err) {
+			assert.EqualValues(t, "cookie", cookie.Name)
+			assert.EqualValues(t, "value", cookie.Value)
+		}
+
+		// Form values
+		if assert.NoError(t, c.Request().ParseForm()) {
+			assert.EqualValues(t, "value", c.Request().FormValue("form"))
+		}
+
+		// Query string
+		assert.EqualValues(t, "value", c.Request().URL.Query()["query"][0])
+		return nil
+	})(c)
+
+	assert.NoError(t, err)
+
+}
diff --git a/router_test.go b/router_test.go
index aafc622..a5e53c0 100644
--- a/router_test.go
+++ b/router_test.go
@@ -750,6 +750,47 @@ func TestRouterMatchAny(t *testing.T) {
 	assert.Equal(t, "joe", c.Param("*"))
 }
 
+// Issue #1739
+func TestRouterMatchAnyPrefixIssue(t *testing.T) {
+	e := New()
+	r := e.router
+
+	// Routes
+	r.Add(http.MethodGet, "/*", func(c Context) error {
+		c.Set("path", c.Path())
+		return nil
+	})
+	r.Add(http.MethodGet, "/users/*", func(c Context) error {
+		c.Set("path", c.Path())
+		return nil
+	})
+	c := e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/", c)
+	c.handler(c)
+	assert.Equal(t, "/*", c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+	r.Find(http.MethodGet, "/users", c)
+	c.handler(c)
+	assert.Equal(t, "/*", c.Get("path"))
+	assert.Equal(t, "users", c.Param("*"))
+
+	r.Find(http.MethodGet, "/users/", c)
+	c.handler(c)
+	assert.Equal(t, "/users/*", c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+	r.Find(http.MethodGet, "/users_prefix", c)
+	c.handler(c)
+	assert.Equal(t, "/*", c.Get("path"))
+	assert.Equal(t, "users_prefix", c.Param("*"))
+
+	r.Find(http.MethodGet, "/users_prefix/", c)
+	c.handler(c)
+	assert.Equal(t, "/*", c.Get("path"))
+	assert.Equal(t, "users_prefix/", c.Param("*"))
+}
+
 // TestRouterMatchAnySlash shall verify finding the best route
 // for any routes with trailing slash requests
 func TestRouterMatchAnySlash(t *testing.T) {