🐛 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
ItsMeSamey 2024-11-25 16:21:36 +05:30 committed by GitHub
parent f8b490f89e
commit f08ebf4335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 107 additions and 82 deletions

33
bind.go
View File

@ -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 {

View File

@ -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())
}

View File

@ -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
View File

@ -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

View File

@ -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