Skip to content
Merged
82 changes: 75 additions & 7 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"slices"
"strings"
"sync"

"github.com/gofiber/fiber/v3/binder"
Expand Down Expand Up @@ -428,6 +429,51 @@ func (b *Bind) Body(out any) error {
return ErrUnprocessableEntity
}

type bindSource int

const (
sourceURI bindSource = iota
sourceBody
sourceQuery
sourceHeader
sourceCookie
)

var bindingPrecedenceCache sync.Map // map[reflect.Type][]bindSource
Comment thread
gaby marked this conversation as resolved.
Outdated

func getBindingPrecedence(t reflect.Type) []bindSource {
if cached, ok := bindingPrecedenceCache.Load(t); ok {
if precedence, ok := cached.([]bindSource); ok {
return precedence
}
}

var precedence []bindSource
for i := 0; i < t.NumField(); i++ {
Comment thread
gaby marked this conversation as resolved.
Outdated
if tag := t.Field(i).Tag.Get("binding_source"); tag != "" {
parts := strings.SplitSeq(tag, ",")
for p := range parts {
switch strings.TrimSpace(p) {
case "uri":
precedence = append(precedence, sourceURI)
case "body":
precedence = append(precedence, sourceBody)
case "query":
precedence = append(precedence, sourceQuery)
case "header":
precedence = append(precedence, sourceHeader)
case "cookie":
precedence = append(precedence, sourceCookie)
}
Comment thread
gaby marked this conversation as resolved.
Outdated
}
break
Comment thread
gaby marked this conversation as resolved.
Outdated
}
}

bindingPrecedenceCache.Store(t, precedence)
return precedence
}

// All binds values from URI params, the request body, the query string,
// headers, and cookies into the provided struct in precedence order.
// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).
Expand All @@ -439,18 +485,40 @@ func (b *Bind) All(out any) error {

outElem := outVal.Elem()

// Precedence: URL Params -> Body -> Query -> Headers -> Cookies
sources := []func(any) error{b.URI}
var sources []func(any) error
customPrecedence := getBindingPrecedence(outElem.Type())

if len(customPrecedence) > 0 {
for _, source := range customPrecedence {
switch source {
case sourceURI:
sources = append(sources, b.URI)
case sourceBody:
if len(b.ctx.Request().Body()) > 0 && len(b.ctx.RequestCtx().Request.Header.ContentType()) > 0 {
sources = append(sources, b.Body)
}
case sourceQuery:
sources = append(sources, b.Query)
case sourceHeader:
sources = append(sources, b.Header)
case sourceCookie:
sources = append(sources, b.Cookie)
}
}
} else {
// Precedence: URL Params -> Body -> Query -> Headers -> Cookies
sources = append(sources, b.URI)

// Check if both Body and Content-Type are set
if len(b.ctx.Request().Body()) > 0 && len(b.ctx.RequestCtx().Request.Header.ContentType()) > 0 {
sources = append(sources, b.Body)
// Check if both Body and Content-Type are set
if len(b.ctx.Request().Body()) > 0 && len(b.ctx.RequestCtx().Request.Header.ContentType()) > 0 {
sources = append(sources, b.Body)
}
sources = append(sources, b.Query, b.Header, b.Cookie)
}
Comment thread
gaby marked this conversation as resolved.
Outdated
sources = append(sources, b.Query, b.Header, b.Cookie)

prevSkip := b.shouldSkipValidation
b.shouldSkipValidation = true

// TODO: Support custom precedence with an optional binding_source tag
// TODO: Create WithOverrideEmptyValues
// Bind from each source, but only update unset fields
for _, bindFunc := range sources {
Expand Down
39 changes: 39 additions & 0 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2911,3 +2911,42 @@ func BenchmarkBind_All(b *testing.B) {
}
}
}

// go test -run Test_Bind_All_CustomPrecedence
func Test_Bind_All_CustomPrecedence(t *testing.T) {
Comment thread
gaby marked this conversation as resolved.
t.Parallel()
app := New()

type CustomPrecedenceReq struct {
BindingSource struct{} `binding_source:"query,header,cookie,body,uri"`
Name string `query:"name" header:"x-name" json:"name"`
}

ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)

// Set data in query, header, and body.
// Since query is the first in our custom precedence, it should win.
ctx.Request().URI().SetQueryString("name=from_query")
ctx.Request().Header.Set("x-name", "from_header")
ctx.Request().Header.SetContentType(MIMEApplicationJSON)
ctx.Request().SetBody([]byte(`{"name":"from_body"}`))

req := new(CustomPrecedenceReq)
require.NoError(t, ctx.Bind().All(req))

// The query parameter should be used because it has the highest precedence.
require.Equal(t, "from_query", req.Name)

// Now try without query but with header and body. Header should win.
ctx2 := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx2)

ctx2.Request().Header.Set("x-name", "from_header")
ctx2.Request().Header.SetContentType(MIMEApplicationJSON)
ctx2.Request().SetBody([]byte(`{"name":"from_body"}`))

req2 := new(CustomPrecedenceReq)
require.NoError(t, ctx2.Bind().All(req2))
require.Equal(t, "from_header", req2.Name)
}
25 changes: 25 additions & 0 deletions docs/api/bind.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,31 @@ app.Post("/users", func(c fiber.Ctx) error {
})
```

#### Custom Precedence

By default, the `All` method binds data in the following precedence order: `URI params -> Body -> Query -> Headers -> Cookies`.
If you need to override this behavior, you can define a custom precedence order for your struct using the `binding_source` tag.

```go title="Example"
type CustomPrecedenceReq struct {
// Specify the priority for binding using the binding_source tag.
// In this example, query parameters take highest priority, followed by headers.
BindingSource struct{} `binding_source:"query,header,cookie,body,uri"`
Comment thread
gaby marked this conversation as resolved.
Outdated
Name string `query:"name" header:"x-name" json:"name"`
}

app.Post("/users", func(c fiber.Ctx) error {
req := new(CustomPrecedenceReq)
if err := c.Bind().All(req); err != nil {
return err
}
// req.Name will take the value from the query parameter if it exists, otherwise header, etc.
return c.JSON(req)
})
```

> **Note:** For maximum performance, Fiber caches the precedence configuration per `reflect.Type`. This ensures the framework maintains zero-allocation efficiency on subsequent requests.
Comment thread
aryan262 marked this conversation as resolved.
Outdated

### Body

Binds the request body to a struct.
Expand Down