mirror of https://github.com/gofiber/fiber.git
🐛 fix: Nil pointer dereference with Must Bind binding (#3171)
* Fix nil pointer dereference with Must Bind binding error if err is nil err.Error() panics (eg. c.Bind().Must().JSON(...) successfully binds but panics * Added returnErr test make sure returnErr works with nil error * Reordered returnErr nil check as in majority of cases we expect err to be nil, this should provide better short-cutting * Use require.NoError * Update bind_test.go * Renamed Must to WithAutoHandling * Update bind.md Added a requested clarification * renamed Should to WithoutAutoHandling and Bind.should to Bind.dontHandle * renamed dontHandle to dontHandleErrs * fixed formatting * fixed a typo * Update binder documentation --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>pull/3217/head
parent
f8b490f89e
commit
f08ebf4335
33
bind.go
33
bind.go
|
@ -19,34 +19,35 @@ type StructValidator interface {
|
|||
|
||||
// Bind struct
|
||||
type Bind struct {
|
||||
ctx Ctx
|
||||
should bool
|
||||
ctx Ctx
|
||||
dontHandleErrs bool
|
||||
}
|
||||
|
||||
// Should To handle binder errors manually, you can prefer Should method.
|
||||
// If you want to handle binder errors manually, you can use `WithoutAutoHandling`.
|
||||
// It's default behavior of binder.
|
||||
func (b *Bind) Should() *Bind {
|
||||
b.should = true
|
||||
func (b *Bind) WithoutAutoHandling() *Bind {
|
||||
b.dontHandleErrs = true
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Must If you want to handle binder errors automatically, you can use Must.
|
||||
// If there's an error it'll return error and 400 as HTTP status.
|
||||
func (b *Bind) Must() *Bind {
|
||||
b.should = false
|
||||
// If you want to handle binder errors automatically, you can use `WithAutoHandling`.
|
||||
// If there's an error, it will return the error and set HTTP status to `400 Bad Request`.
|
||||
// You must still return on error explicitly
|
||||
func (b *Bind) WithAutoHandling() *Bind {
|
||||
b.dontHandleErrs = false
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Check Should/Must errors and return it by usage.
|
||||
// Check WithAutoHandling/WithoutAutoHandling errors and return it by usage.
|
||||
func (b *Bind) returnErr(err error) error {
|
||||
if !b.should {
|
||||
b.ctx.Status(StatusBadRequest)
|
||||
return NewError(StatusBadRequest, "Bad request: "+err.Error())
|
||||
if err == nil || b.dontHandleErrs {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
b.ctx.Status(StatusBadRequest)
|
||||
return NewError(StatusBadRequest, "Bad request: "+err.Error())
|
||||
}
|
||||
|
||||
// Struct validation.
|
||||
|
@ -62,7 +63,7 @@ func (b *Bind) validateStruct(out any) error {
|
|||
// Custom To use custom binders, you have to use this method.
|
||||
// You can register them from RegisterCustomBinder method of Fiber instance.
|
||||
// They're checked by name, if it's not found, it will return an error.
|
||||
// NOTE: Should/Must is still valid for Custom binders.
|
||||
// NOTE: WithAutoHandling/WithAutoHandling is still valid for Custom binders.
|
||||
func (b *Bind) Custom(name string, dest any) error {
|
||||
binders := b.ctx.App().customBinders
|
||||
for _, customBinder := range binders {
|
||||
|
@ -92,7 +93,7 @@ func (b *Bind) RespHeader(out any) error {
|
|||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// Cookie binds the requesr cookie strings into the struct, map[string]string and map[string][]string.
|
||||
// Cookie binds the request cookie strings into the struct, map[string]string and map[string][]string.
|
||||
// NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie.
|
||||
func (b *Bind) Cookie(out any) error {
|
||||
if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
|
||||
|
|
15
bind_test.go
15
bind_test.go
|
@ -19,6 +19,15 @@ import (
|
|||
|
||||
const helloWorld = "hello world"
|
||||
|
||||
// go test -run Test_returnErr -v
|
||||
func Test_returnErr(t *testing.T) {
|
||||
app := New()
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
err := c.Bind().WithAutoHandling().returnErr(nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// go test -run Test_Bind_Query -v
|
||||
func Test_Bind_Query(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -1616,8 +1625,8 @@ func Test_Bind_CustomBinder(t *testing.T) {
|
|||
require.Equal(t, "john", d.Name)
|
||||
}
|
||||
|
||||
// go test -run Test_Bind_Must
|
||||
func Test_Bind_Must(t *testing.T) {
|
||||
// go test -run Test_Bind_WithAutoHandling
|
||||
func Test_Bind_WithAutoHandling(t *testing.T) {
|
||||
app := New()
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
|
@ -1626,7 +1635,7 @@ func Test_Bind_Must(t *testing.T) {
|
|||
}
|
||||
rq := new(RequiredQuery)
|
||||
c.Request().URI().SetQueryString("")
|
||||
err := c.Bind().Must().Query(rq)
|
||||
err := c.Bind().WithAutoHandling().Query(rq)
|
||||
require.Equal(t, StatusBadRequest, c.Response().StatusCode())
|
||||
require.Equal(t, "Bad request: bind: name is empty", err.Error())
|
||||
}
|
||||
|
|
120
binder/README.md
120
binder/README.md
|
@ -1,17 +1,19 @@
|
|||
# Fiber Binders
|
||||
|
||||
Binder is a new request/response binding feature for Fiber. Against the old Fiber parsers, it supports custom binder registration, struct validation, `map[string]string`, `map[string][]string`, and more. It's introduced in Fiber v3 and a replacement of:
|
||||
**Binder** is a new request/response binding feature for Fiber introduced in Fiber v3. It replaces the old Fiber parsers and offers enhanced capabilities such as custom binder registration, struct validation, support for `map[string]string`, `map[string][]string`, and more. Binder replaces the following components:
|
||||
|
||||
- BodyParser
|
||||
- ParamsParser
|
||||
- GetReqHeaders
|
||||
- GetRespHeaders
|
||||
- AllParams
|
||||
- QueryParser
|
||||
- ReqHeaderParser
|
||||
- `BodyParser`
|
||||
- `ParamsParser`
|
||||
- `GetReqHeaders`
|
||||
- `GetRespHeaders`
|
||||
- `AllParams`
|
||||
- `QueryParser`
|
||||
- `ReqHeaderParser`
|
||||
|
||||
## Default Binders
|
||||
|
||||
Fiber provides several default binders out of the box:
|
||||
|
||||
- [Form](form.go)
|
||||
- [Query](query.go)
|
||||
- [URI](uri.go)
|
||||
|
@ -23,12 +25,12 @@ Binder is a new request/response binding feature for Fiber. Against the old Fibe
|
|||
|
||||
## Guides
|
||||
|
||||
### Binding into the Struct
|
||||
### Binding into a Struct
|
||||
|
||||
Fiber supports binding into the struct with [gorilla/schema](https://github.com/gorilla/schema). Here's an example:
|
||||
Fiber supports binding request data directly into a struct using [gorilla/schema](https://github.com/gorilla/schema). Here's an example:
|
||||
|
||||
```go
|
||||
// Field names should start with an uppercase letter
|
||||
// Field names must start with an uppercase letter
|
||||
type Person struct {
|
||||
Name string `json:"name" xml:"name" form:"name"`
|
||||
Pass string `json:"pass" xml:"pass" form:"pass"`
|
||||
|
@ -41,56 +43,63 @@ app.Post("/", func(c fiber.Ctx) error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Println(p.Name) // john
|
||||
log.Println(p.Pass) // doe
|
||||
log.Println(p.Name) // Output: john
|
||||
log.Println(p.Pass) // Output: doe
|
||||
|
||||
// ...
|
||||
// Additional logic...
|
||||
})
|
||||
|
||||
// Run tests with the following curl commands:
|
||||
|
||||
// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000
|
||||
// JSON
|
||||
curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000
|
||||
|
||||
// curl -X POST -H "Content-Type: application/xml" --data "<login><name>john</name><pass>doe</pass></login>" localhost:3000
|
||||
// XML
|
||||
curl -X POST -H "Content-Type: application/xml" --data "<login><name>john</name><pass>doe</pass></login>" localhost:3000
|
||||
|
||||
// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000
|
||||
// URL-Encoded Form
|
||||
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000
|
||||
|
||||
// curl -X POST -F name=john -F pass=doe http://localhost:3000
|
||||
// Multipart Form
|
||||
curl -X POST -F name=john -F pass=doe http://localhost:3000
|
||||
|
||||
// curl -X POST "http://localhost:3000/?name=john&pass=doe"
|
||||
// Query Parameters
|
||||
curl -X POST "http://localhost:3000/?name=john&pass=doe"
|
||||
```
|
||||
|
||||
### Binding into the Map
|
||||
### Binding into a Map
|
||||
|
||||
Fiber supports binding into the `map[string]string` or `map[string][]string`. Here's an example:
|
||||
Fiber allows binding request data into a `map[string]string` or `map[string][]string`. Here's an example:
|
||||
|
||||
```go
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
p := make(map[string][]string)
|
||||
params := make(map[string][]string)
|
||||
|
||||
if err := c.Bind().Query(p); err != nil {
|
||||
if err := c.Bind().Query(params); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println(p["name"]) // john
|
||||
log.Println(p["pass"]) // doe
|
||||
log.Println(p["products"]) // [shoe, hat]
|
||||
log.Println(params["name"]) // Output: [john]
|
||||
log.Println(params["pass"]) // Output: [doe]
|
||||
log.Println(params["products"]) // Output: [shoe hat]
|
||||
|
||||
// ...
|
||||
// Additional logic...
|
||||
return nil
|
||||
})
|
||||
|
||||
// Run tests with the following curl command:
|
||||
|
||||
// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat"
|
||||
curl "http://localhost:3000/?name=john&pass=doe&products=shoe&products=hat"
|
||||
```
|
||||
|
||||
### Behaviors of Should/Must
|
||||
### Automatic Error Handling with `WithAutoHandling`
|
||||
|
||||
Normally, Fiber returns binder error directly. However; if you want to handle it automatically, you can prefer `Must()`.
|
||||
By default, Fiber returns binder errors directly. To handle errors automatically and return a `400 Bad Request` status, use the `WithAutoHandling()` method.
|
||||
|
||||
If there's an error it'll return error and 400 as HTTP status. Here's an example for it:
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
// Field names should start with an uppercase letter
|
||||
// Field names must start with an uppercase letter
|
||||
type Person struct {
|
||||
Name string `json:"name,required"`
|
||||
Pass string `json:"pass"`
|
||||
|
@ -99,23 +108,24 @@ type Person struct {
|
|||
app.Get("/", func(c fiber.Ctx) error {
|
||||
p := new(Person)
|
||||
|
||||
if err := c.Bind().Must().JSON(p); err != nil {
|
||||
if err := c.Bind().WithAutoHandling().JSON(p); err != nil {
|
||||
return err
|
||||
// Status code: 400
|
||||
// Automatically returns status code 400
|
||||
// Response: Bad request: name is empty
|
||||
}
|
||||
|
||||
// ...
|
||||
// Additional logic...
|
||||
return nil
|
||||
})
|
||||
|
||||
// Run tests with the following curl command:
|
||||
|
||||
// curl -X GET -H "Content-Type: application/json" --data "{\"pass\":\"doe\"}" localhost:3000
|
||||
curl -X GET -H "Content-Type: application/json" --data "{\"pass\":\"doe\"}" localhost:3000
|
||||
```
|
||||
|
||||
### Defining Custom Binder
|
||||
### Defining a Custom Binder
|
||||
|
||||
We didn't add much binder to make Fiber codebase minimal. If you want to use your own binders, it's easy to register and use them. Here's an example for TOML binder.
|
||||
Fiber maintains a minimal codebase by not including every possible binder. If you need to use a custom binder, you can easily register and utilize it. Here's an example of creating a `toml` binder.
|
||||
|
||||
```go
|
||||
type Person struct {
|
||||
|
@ -147,24 +157,26 @@ func main() {
|
|||
return err
|
||||
}
|
||||
|
||||
// or you can use like:
|
||||
// Alternatively, specify the custom binder:
|
||||
// if err := c.Bind().Custom("toml", out); err != nil {
|
||||
// return err
|
||||
// return err
|
||||
// }
|
||||
|
||||
return c.SendString(out.Pass) // test
|
||||
return c.SendString(out.Pass) // Output: test
|
||||
})
|
||||
|
||||
app.Listen(":3000")
|
||||
}
|
||||
|
||||
// curl -X GET -H "Content-Type: application/toml" --data "name = 'bar'
|
||||
// pass = 'test'" localhost:3000
|
||||
// Run tests with the following curl command:
|
||||
|
||||
curl -X GET -H "Content-Type: application/toml" --data "name = 'bar'
|
||||
pass = 'test'" localhost:3000
|
||||
```
|
||||
|
||||
### Defining Custom Validator
|
||||
### Defining a Custom Validator
|
||||
|
||||
All Fiber binders supporting struct validation if you defined validator inside of the config. You can create own validator, or use [go-playground/validator](https://github.com/go-playground/validator), [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation)... Here's an example of simple custom validator:
|
||||
All Fiber binders support struct validation if a validator is defined in the configuration. You can create your own validator or use existing ones like [go-playground/validator](https://github.com/go-playground/validator) or [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation). Here's an example of a simple custom validator:
|
||||
|
||||
```go
|
||||
type Query struct {
|
||||
|
@ -174,27 +186,29 @@ type Query struct {
|
|||
type structValidator struct{}
|
||||
|
||||
func (v *structValidator) Engine() any {
|
||||
return ""
|
||||
return nil // Implement if using an external validation engine
|
||||
}
|
||||
|
||||
func (v *structValidator) ValidateStruct(out any) error {
|
||||
out = reflect.ValueOf(out).Elem().Interface()
|
||||
sq := out.(Query)
|
||||
data := reflect.ValueOf(out).Elem().Interface()
|
||||
query := data.(Query)
|
||||
|
||||
if sq.Name != "john" {
|
||||
return errors.New("you should have entered right name!")
|
||||
if query.Name != "john" {
|
||||
return errors.New("you should have entered the correct name!")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := fiber.New(fiber.Config{StructValidator: &structValidator{}})
|
||||
app := fiber.New(fiber.Config{
|
||||
StructValidator: &structValidator{},
|
||||
})
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
out := new(Query)
|
||||
if err := c.Bind().Query(out); err != nil {
|
||||
return err // you should have entered right name!
|
||||
return err // Returns: you should have entered the correct name!
|
||||
}
|
||||
return c.SendString(out.Name)
|
||||
})
|
||||
|
@ -204,5 +218,5 @@ func main() {
|
|||
|
||||
// Run tests with the following curl command:
|
||||
|
||||
// curl "http://localhost:3000/?name=efe"
|
||||
curl "http://localhost:3000/?name=efe"
|
||||
```
|
||||
|
|
8
ctx.go
8
ctx.go
|
@ -640,7 +640,7 @@ func (c *DefaultCtx) Get(key string, defaultValue ...string) string {
|
|||
}
|
||||
|
||||
// GetReqHeader returns the HTTP request header specified by filed.
|
||||
// This function is generic and can handle differnet headers type values.
|
||||
// This function is generic and can handle different headers type values.
|
||||
func GetReqHeader[V GenericType](c Ctx, key string, defaultValue ...V) V {
|
||||
var v V
|
||||
return genericParseType[V](c.App().getString(c.Request().Header.Peek(key)), v, defaultValue...)
|
||||
|
@ -1083,7 +1083,7 @@ func (c *DefaultCtx) Params(key string, defaultValue ...string) string {
|
|||
}
|
||||
|
||||
// Params is used to get the route parameters.
|
||||
// This function is generic and can handle differnet route parameters type values.
|
||||
// This function is generic and can handle different route parameters type values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
|
@ -1860,8 +1860,8 @@ func (c *DefaultCtx) IsFromLocal() bool {
|
|||
func (c *DefaultCtx) Bind() *Bind {
|
||||
if c.bind == nil {
|
||||
c.bind = &Bind{
|
||||
ctx: c,
|
||||
should: true,
|
||||
ctx: c,
|
||||
dontHandleErrs: true,
|
||||
}
|
||||
}
|
||||
return c.bind
|
||||
|
|
|
@ -458,22 +458,23 @@ The `MIMETypes` method is used to check if the custom binder should be used for
|
|||
|
||||
For more control over error handling, you can use the following methods.
|
||||
|
||||
### Must
|
||||
### WithAutoHandling
|
||||
|
||||
If you want to handle binder errors automatically, you can use `Must`.
|
||||
If you want to handle binder errors automatically, you can use `WithAutoHandling`.
|
||||
If there's an error, it will return the error and set HTTP status to `400 Bad Request`.
|
||||
This function does NOT panic therefor you must still return on error explicitly
|
||||
|
||||
```go title="Signature"
|
||||
func (b *Bind) Must() *Bind
|
||||
func (b *Bind) WithAutoHandling() *Bind
|
||||
```
|
||||
|
||||
### Should
|
||||
### WithoutAutoHandling
|
||||
|
||||
To handle binder errors manually, you can use the `Should` method.
|
||||
To handle binder errors manually, you can use the `WithoutAutoHandling` method.
|
||||
It's the default behavior of the binder.
|
||||
|
||||
```go title="Signature"
|
||||
func (b *Bind) Should() *Bind
|
||||
func (b *Bind) WithoutAutoHandling() *Bind
|
||||
```
|
||||
|
||||
## SetParserDecoder
|
||||
|
|
Loading…
Reference in New Issue