From ee85a7c11c0e0d658f8370d63103a228849cfde5 Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Sat, 2 Jan 2021 22:39:52 +0200 Subject: [PATCH 1/3] Improve request binding documentation. See Echo pull request #1736 (https://github.com/labstack/echo/pull/1736) --- website/content/guide/request.md | 86 ++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/website/content/guide/request.md b/website/content/guide/request.md index 5a641371..764339ca 100644 --- a/website/content/guide/request.md +++ b/website/content/guide/request.md @@ -6,21 +6,100 @@ description = "Handling HTTP request in Echo" parent = "guide" +++ -## Bind Data +## Bind request data with bind functions -Echo provides following method to bind data from different sources (path params, query params, request body) to structure +Following functions provide handful of methods for binding to Go native types from request query or path parameters. + +* `echo.QueryParamsBinder(c)` - binds query parameters (source URL) +* `echo.PathParamsBinder(c)` - binds path parameters (source URL) +* `echo.FormFieldBinder(c)` - binds form fields (source URL + body). See also [Request.ParseForm](https://golang.org/pkg/net/http/#Request.ParseForm). + +```go +// url = "/api/search?active=true&id=1&id=2&id=3&length=25" +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 +``` + +### Methods +`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. + +### Supported types + +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 + +For every supported type there are following methods: + +* `("param", &destination)` - if parameter value exists then binds it to given destination of that type i.e `Int64(...)`. +* `Must("param", &destination)` - parameter value is required to exist, binds it to given destination of that type i.e `MustInt64(...)`. +* `s("param", &destination)` - (for slices) if parameter values exists then binds it to given destination of that type i.e `I`nt64s(...)`. +* `Musts("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}` + +## Bind request data with struct tags + +Echo provides following method to bind data from different sources (path params, query params, request body) to structure using `Context#Bind(i interface{})` method. The default binder supports decoding application/json, application/xml and application/x-www-form-urlencoded data based on the Content-Type header. +Supported struct tags: +* `query` - source is request query parameters. +* `param` - source is route path parameter. +* `form` - source is form. Values are taken from query and request body. Uses Go standard library form parsing. +* `json` - source is request body. Uses Go [json](https://golang.org/pkg/encoding/json/) package fo unmarshalling. +* `xml` - source is request body. Uses Go [xml](https://golang.org/pkg/encoding/xml/) package fo unmarshalling. + +```go +type User struct { + ID string `path:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + Request data is binded to the struct in given order: 1. Path parameters -2. Query parameters +2. Query parameters (only for GET/DELETE methods) 3. Request body Notes: +* For `query`, `param`, `form` **only** fields **with** tags are binded. +* For `json` and `xml` can bind to *public* fields without tags but this is by their standard library implementation. * Each step can overwrite binded fields from the previous step. This means if your json request has query param `&name=query` and body is `{"name": "body"}` then the result will be `User{Name: "body"}`. * To avoid security flaws try to avoid passing binded structs directly to other methods if @@ -49,6 +128,7 @@ Notes: } ``` +### Example 1 Example below binds the request payload into `User` struct based on tags: ```go From 102f1ed1c1fce0b5f4e041bc8bf715b584093b8f Mon Sep 17 00:00:00 2001 From: Roland Lammel Date: Sun, 31 Jan 2021 21:22:30 +0100 Subject: [PATCH 2/3] Split binding to dedicated guide, some wording improvements --- website/content/guide/binding.md | 205 ++++++++++++++++++++++ website/content/guide/request.md | 287 ++++--------------------------- 2 files changed, 243 insertions(+), 249 deletions(-) create mode 100644 website/content/guide/binding.md diff --git a/website/content/guide/binding.md b/website/content/guide/binding.md new file mode 100644 index 00000000..76af4014 --- /dev/null +++ b/website/content/guide/binding.md @@ -0,0 +1,205 @@ ++++ +title = "Binding Request Data" +description = "Binding request data" +[menu.main] + name = "Binding" + parent = "guide" ++++ + +## Bind using struct tags + +Echo provides following method to bind data from different sources (path params, query params, request body) to structure using +`Context#Bind(i interface{})` method. +The default binder supports decoding application/json, application/xml and +application/x-www-form-urlencoded data based on the Content-Type header. + +In the struct definitions each field can be tagged to restrict binding to specific source. + +* `query` - source is request query parameters. +* `param` - source is route path parameter. +* `form` - source is form. Values are taken from query and request body. Uses Go standard library form parsing. +* `json` - source is request body. Uses Go [json](https://golang.org/pkg/encoding/json/) package fo unmarshalling. +* `xml` - source is request body. Uses Go [xml](https://golang.org/pkg/encoding/xml/) package fo unmarshalling. + +```go +type User struct { + ID string `path:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +Request data is binded to the struct in given order: + +1. Path parameters +2. Query parameters (only for GET/DELETE methods) +3. Request body + +Notes: + +* For `query`, `param`, `form` **only** fields **with** tags are bound. +* For `json` and `xml` can bind to *public* fields without tags but this is by their standard library implementation. +* Each step can overwrite binded fields from the previous step. This means if your json request has query param + `&name=query` and body is `{"name": "body"}` then the result will be `User{Name: "body"}`. +* To avoid security flaws try to avoid passing binded structs directly to other methods if + these structs contain fields that should not be bindable. It is advisable to have separate struct for binding and map it + explicitly to your business struct. Consider what will happen if your binded struct has public + field `IsAdmin bool` and request body would contain `{IsAdmin: true, Name: "hacker"}`. +* When binding forms take note that Echo implementation uses standard library form parsing which parses form data + from BOTH URL and BODY if content type is not MIMEMultipartForm. See documentation for [non-MIMEMultipartForm](https://golang.org/pkg/net/http/#Request.ParseForm) + and [MIMEMultipartForm](https://golang.org/pkg/net/http/#Request.ParseMultipartForm) +* To bind data only from request body use following code + ```go + if err := (&DefaultBinder{}).BindBody(c, &payload); err != nil { + return err + } + ``` +* To bind data only from query parameters use following code + ```go + if err := (&DefaultBinder{}).BindQueryParams(c, &payload); err != nil { + return err + } + ``` +* To bind data only from path parameters use following code + ```go + if err := (&DefaultBinder{}).BindPathParams(c, &payload); err != nil { + return err + } + ``` + +### Example + +Example below binds the request payload into `User` struct based on tags: + +```go +// User +type User struct { + Name string `json:"name" form:"name" query:"name"` + Email string `json:"email" form:"email" query:"email"` +} +``` + +```go +// Handler +func(c echo.Context) (err error) { + u := new(User) + if err = c.Bind(u); err != nil { + return + } + // To avoid security flaws try to avoid passing binded structs directly to other methods + // if these structs contain fields that should not be bindable. + user := UserDTO{ + Name: u.Name, + Email: u.Email, + IsAdmin: false // because you could accidentally expose fields that should not be bind + } + executeSomeBusinessLogic(user) + + return c.JSON(http.StatusOK, u) +} +``` + +### JSON Data + +```sh +curl -X POST http://localhost:1323/users \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe","email":"joe@labstack"}' +``` + +### Form Data + +```sh +curl -X POST http://localhost:1323/users \ + -d 'name=Joe' \ + -d 'email=joe@labstack.com' +``` + +### Query Parameters + +```sh +curl -X GET http://localhost:1323/users\?name\=Joe\&email\=joe@labstack.com +``` + +## Fast binding with dedicated helpers + +For binding data found in a request a handful of helper functions are provided. This will allow binding of query parameters, path parameters or data found in the body like forms or JSON data. + +Following functions provide a handful of methods for binding to Go native types from request query or path parameters. These binders offer a fluent syntax and can be chained to configure, execute binding and handle errors. + +* `echo.QueryParamsBinder(c)` - binds query parameters (source URL) +* `echo.PathParamsBinder(c)` - binds path parameters (source URL) +* `echo.FormFieldBinder(c)` - binds form fields (source URL + body). See also [Request.ParseForm](https://golang.org/pkg/net/http/#Request.ParseForm). + +A binder is usually completed by `BindError()` or `BindErrors()` which returns errors if binding fails. +With `FailFast()` the binder can be configured stop binding on the first error or continue binding for +the binder call chain. Fail fast is enabled by default and should be disabled when using `BindErrors()`. + +`BindError()` returns the first bind error from binder and resets all errors in this binder. +`BindErrors()` returns all bind errors from binder and resets errors in binder. + +```go +// url = "/api/search?active=true&id=1&id=2&id=3&length=25" +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 +``` + +### Supported types + +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 + +For every supported type there are following methods: + +* `("param", &destination)` - if parameter value exists then binds it to given destination of that type i.e `Int64(...)`. +* `Must("param", &destination)` - parameter value is required to exist, binds it to given destination of that type i.e `MustInt64(...)`. +* `s("param", &destination)` - (for slices) if parameter values exists then binds it to given destination of that type i.e `Int64s(...)`. +* `Musts("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. For example URL `/api/search?id=1,2,3&id=1` can be bind to `[]int64{1,2,3,1}` + +## Custom Binder + +Custom binder can be registered using `Echo#Binder`. + +```go +type CustomBinder struct {} + +func (cb *CustomBinder) Bind(i interface{}, c echo.Context) (err error) { + // You may use default binder + db := new(echo.DefaultBinder) + if err = db.Bind(i, c); err != echo.ErrUnsupportedMediaType { + return + } + + // Define your custom implementation here + return +} +``` diff --git a/website/content/guide/request.md b/website/content/guide/request.md index 764339ca..989213eb 100644 --- a/website/content/guide/request.md +++ b/website/content/guide/request.md @@ -6,243 +6,33 @@ description = "Handling HTTP request in Echo" parent = "guide" +++ -## Bind request data with bind functions - -Following functions provide handful of methods for binding to Go native types from request query or path parameters. - -* `echo.QueryParamsBinder(c)` - binds query parameters (source URL) -* `echo.PathParamsBinder(c)` - binds path parameters (source URL) -* `echo.FormFieldBinder(c)` - binds form fields (source URL + body). See also [Request.ParseForm](https://golang.org/pkg/net/http/#Request.ParseForm). - -```go -// url = "/api/search?active=true&id=1&id=2&id=3&length=25" -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 -``` - -### Methods -`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. - -### Supported types - -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 - -For every supported type there are following methods: - -* `("param", &destination)` - if parameter value exists then binds it to given destination of that type i.e `Int64(...)`. -* `Must("param", &destination)` - parameter value is required to exist, binds it to given destination of that type i.e `MustInt64(...)`. -* `s("param", &destination)` - (for slices) if parameter values exists then binds it to given destination of that type i.e `I`nt64s(...)`. -* `Musts("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}` - -## Bind request data with struct tags - -Echo provides following method to bind data from different sources (path params, query params, request body) to structure using -`Context#Bind(i interface{})` method. -The default binder supports decoding application/json, application/xml and -application/x-www-form-urlencoded data based on the Content-Type header. - -Supported struct tags: -* `query` - source is request query parameters. -* `param` - source is route path parameter. -* `form` - source is form. Values are taken from query and request body. Uses Go standard library form parsing. -* `json` - source is request body. Uses Go [json](https://golang.org/pkg/encoding/json/) package fo unmarshalling. -* `xml` - source is request body. Uses Go [xml](https://golang.org/pkg/encoding/xml/) package fo unmarshalling. - -```go -type User struct { - ID string `path:"id" query:"id" form:"id" json:"id" xml:"id"` -} -``` - -Request data is binded to the struct in given order: - -1. Path parameters -2. Query parameters (only for GET/DELETE methods) -3. Request body - -Notes: - -* For `query`, `param`, `form` **only** fields **with** tags are binded. -* For `json` and `xml` can bind to *public* fields without tags but this is by their standard library implementation. -* Each step can overwrite binded fields from the previous step. This means if your json request has query param - `&name=query` and body is `{"name": "body"}` then the result will be `User{Name: "body"}`. -* To avoid security flaws try to avoid passing binded structs directly to other methods if - these structs contain fields that should not be bindable. It is advisable to have separate struct for binding and map it - explicitly to your business struct. Consider what will happen if your binded struct has public - field `IsAdmin bool` and request body would contain `{IsAdmin: true, Name: "hacker"}`. -* When binding forms take note that Echo implementation uses standard library form parsing which parses form data - from BOTH URL and BODY if content type is not MIMEMultipartForm. See documentation for [non-MIMEMultipartForm](https://golang.org/pkg/net/http/#Request.ParseForm) - and [MIMEMultipartForm](https://golang.org/pkg/net/http/#Request.ParseMultipartForm) -* To bind data only from request body use following code - ```go - if err := (&DefaultBinder{}).BindBody(c, &payload); err != nil { - return err - } - ``` -* To bind data only from query parameters use following code - ```go - if err := (&DefaultBinder{}).BindQueryParams(c, &payload); err != nil { - return err - } - ``` -* To bind data only from path parameters use following code - ```go - if err := (&DefaultBinder{}).BindPathParams(c, &payload); err != nil { - return err - } - ``` - -### Example 1 -Example below binds the request payload into `User` struct based on tags: - -```go -// User -type User struct { - Name string `json:"name" form:"name" query:"name"` - Email string `json:"email" form:"email" query:"email"` -} -``` - -```go -// Handler -func(c echo.Context) (err error) { - u := new(User) - if err = c.Bind(u); err != nil { - return - } - // To avoid security flaws try to avoid passing binded structs directly to other methods - // if these structs contain fields that should not be bindable. - user := UserDTO{ - Name: u.Name, - Email: u.Email, - IsAdmin: false // because you could accidentally expose fields that should not be bind - } - executeSomeBusinessLogic(user) - - return c.JSON(http.StatusOK, u) -} -``` - -### JSON Data - -```sh -curl \ - -X POST \ - http://localhost:1323/users \ - -H 'Content-Type: application/json' \ - -d '{"name":"Joe","email":"joe@labstack"}' -``` - -### Form Data - -```sh -curl \ - -X POST \ - http://localhost:1323/users \ - -d 'name=Joe' \ - -d 'email=joe@labstack.com' -``` - -### Query Parameters - -```sh -curl \ - -X GET \ - http://localhost:1323/users\?name\=Joe\&email\=joe@labstack.com -``` - -## Custom Binder - -Custom binder can be registered using `Echo#Binder`. - -*Example* - -```go -type CustomBinder struct {} - -func (cb *CustomBinder) Bind(i interface{}, c echo.Context) (err error) { - // You may use default binder - db := new(echo.DefaultBinder) - if err = db.Bind(i, c); err != echo.ErrUnsupportedMediaType { - return - } - - // Define your custom implementation - - return -} -``` - ## Retrieve Data ### Form Data Form data can be retrieved by name using `Context#FormValue(name string)`. -*Example* - ```go // Handler func(c echo.Context) error { - name := c.FormValue("name") - return c.String(http.StatusOK, name) + name := c.FormValue("name") + return c.String(http.StatusOK, name) } ``` ```sh -curl \ - -X POST \ - http://localhost:1323 \ - -d 'name=Joe' +curl -X POST http://localhost:1323 -d 'name=Joe' ``` To bind a custom data type, you can implement `Echo#BindUnmarshaler` interface. -*Example* - ```go type Timestamp time.Time func (t *Timestamp) UnmarshalParam(src string) error { - ts, err := time.Parse(time.RFC3339, src) - *t = Timestamp(ts) - return err + ts, err := time.Parse(time.RFC3339, src) + *t = Timestamp(ts) + return err } ``` @@ -250,13 +40,11 @@ func (t *Timestamp) UnmarshalParam(src string) error { Query parameters can be retrieved by name using `Context#QueryParam(name string)`. -*Example* - ```go // Handler func(c echo.Context) error { - name := c.QueryParam("name") - return c.String(http.StatusOK, name) + name := c.QueryParam("name") + return c.String(http.StatusOK, name) }) ``` @@ -272,19 +60,22 @@ Similar to form data, custom data type can be bind using `Context#QueryParam(nam Registered path parameters can be retrieved by name using `Context#Param(name string) string`. -*Example* - ```go e.GET("/users/:name", func(c echo.Context) error { - name := c.Param("name") - return c.String(http.StatusOK, name) + name := c.Param("name") + return c.String(http.StatusOK, name) }) ``` ```sh -$ curl http://localhost:1323/users/Joe +curl http://localhost:1323/users/Joe ``` +### Binding Data + +Also binding of request data to native Go structs and variables is supported. +See [Binding Data]({{< ref "guide/binding.md">}}) + ## Validate Data Echo doesn't have built-in data validation capabilities, however, you can register @@ -294,41 +85,39 @@ Example below uses https://github.com/go-playground/validator framework for vali ```go type ( - User struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - } + User struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + } - CustomValidator struct { - validator *validator.Validate - } + CustomValidator struct { + validator *validator.Validate + } ) func (cv *CustomValidator) Validate(i interface{}) error { - return echo.NewHTTPError(http.StatusInternalServerError, cv.validator.Struct(i).Error()) + return echo.NewHTTPError(http.StatusInternalServerError, cv.validator.Struct(i).Error()) } func main() { - e := echo.New() - e.Validator = &CustomValidator{validator: validator.New()} - e.POST("/users", func(c echo.Context) (err error) { - u := new(User) - if err = c.Bind(u); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - if err = c.Validate(u); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - return c.JSON(http.StatusOK, u) - }) - e.Logger.Fatal(e.Start(":1323")) + e := echo.New() + e.Validator = &CustomValidator{validator: validator.New()} + e.POST("/users", func(c echo.Context) (err error) { + u := new(User) + if err = c.Bind(u); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err = c.Validate(u); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSON(http.StatusOK, u) + }) + e.Logger.Fatal(e.Start(":1323")) } ``` ```sh -curl \ - -X POST \ - http://localhost:1323/users \ +curl -X POST http://localhost:1323/users \ -H 'Content-Type: application/json' \ -d '{"name":"Joe","email":"joe@invalid-domain"}' {"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"} From 85579fc22442416456eaa624c3f8dfa6d8c5ae7d Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 1 Feb 2021 11:21:17 +0200 Subject: [PATCH 3/3] Bind example more alike with other router examples. Mention `UnixTimeNano()` as supported fluent binder function --- website/content/guide/binding.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/content/guide/binding.md b/website/content/guide/binding.md index 76af4014..15454335 100644 --- a/website/content/guide/binding.md +++ b/website/content/guide/binding.md @@ -78,8 +78,7 @@ type User struct { ``` ```go -// Handler -func(c echo.Context) (err error) { +e.POST("/users", func(c echo.Context) (err error) { u := new(User) if err = c.Bind(u); err != nil { return @@ -174,6 +173,7 @@ Types that are supported: * 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 For every supported type there are following methods: