66 "fmt"
77 "reflect"
88 "slices"
9+ "strings"
910 "sync"
1011
1112 "github.com/gofiber/fiber/v3/binder"
@@ -428,6 +429,75 @@ func (b *Bind) Body(out any) error {
428429 return ErrUnprocessableEntity
429430}
430431
432+ type bindSource int
433+
434+ const (
435+ sourceURI bindSource = iota
436+ sourceBody
437+ sourceQuery
438+ sourceHeader
439+ sourceCookie
440+ )
441+
442+ type cachedPrecedence struct {
443+ err error
444+ sources []bindSource
445+ }
446+
447+ var bindingPrecedenceCache sync.Map // map[reflect.Type]cachedPrecedence
448+
449+ func getBindingPrecedence (t reflect.Type ) ([]bindSource , error ) {
450+ if cached , ok := bindingPrecedenceCache .Load (t ); ok {
451+ if cp , ok := cached .(cachedPrecedence ); ok {
452+ return cp .sources , cp .err
453+ }
454+ }
455+ var precedence []bindSource
456+ var tagFound bool
457+ for i := range t .NumField () {
458+ if tag := t .Field (i ).Tag .Get ("binding_source" ); tag != "" {
459+ if tagFound {
460+ err := fmt .Errorf ("multiple binding_source tags found on struct %s" , t .Name ())
461+ bindingPrecedenceCache .Store (t , cachedPrecedence {err : err , sources : nil })
462+ return nil , err
463+ }
464+ tagFound = true
465+
466+ parts := strings .SplitSeq (tag , "," )
467+ for p := range parts {
468+ sourceName := strings .TrimSpace (p )
469+ if sourceName == "" {
470+ continue
471+ }
472+ var source bindSource
473+ switch sourceName {
474+ case "uri" :
475+ source = sourceURI
476+ case "body" :
477+ source = sourceBody
478+ case "query" :
479+ source = sourceQuery
480+ case "header" :
481+ source = sourceHeader
482+ case "cookie" :
483+ source = sourceCookie
484+ default :
485+ err := fmt .Errorf ("unknown binding_source %q" , sourceName )
486+ bindingPrecedenceCache .Store (t , cachedPrecedence {err : err , sources : nil })
487+ return nil , err
488+ }
489+
490+ // check for duplicates
491+ if ! slices .Contains (precedence , source ) {
492+ precedence = append (precedence , source )
493+ }
494+ }
495+ }
496+ }
497+ bindingPrecedenceCache .Store (t , cachedPrecedence {err : nil , sources : precedence })
498+ return precedence , nil
499+ }
500+
431501// All binds values from URI params, the request body, the query string,
432502// headers, and cookies into the provided struct in precedence order.
433503// Returns *BindError on parse failure (manual mode) or *Error with status 400 (auto-handling mode).
@@ -439,18 +509,48 @@ func (b *Bind) All(out any) error {
439509
440510 outElem := outVal .Elem ()
441511
442- // Precedence: URL Params -> Body -> Query -> Headers -> Cookies
443- sources := []func (any ) error {b .URI }
512+ sources := make ([]func (any ) error , 0 , 5 )
513+ customPrecedence , err := getBindingPrecedence (outElem .Type ())
514+ if err != nil {
515+ // Note: A malformed binding_source tag is a programmer error, not a client error.
516+ // Returning the raw error here bypasses b.returnErr, intentionally resulting in a 500
517+ // rather than a 400 even in auto-handling mode.
518+ return err
519+ }
444520
445- // Check if both Body and Content-Type are set
446- if len (b .ctx .Request ().Body ()) > 0 && len (b .ctx .RequestCtx ().Request .Header .ContentType ()) > 0 {
447- sources = append (sources , b .Body )
521+ hasBody := len (b .ctx .Request ().Body ()) > 0 && len (b .ctx .RequestCtx ().Request .Header .ContentType ()) > 0
522+
523+ if len (customPrecedence ) > 0 {
524+ for _ , source := range customPrecedence {
525+ switch source {
526+ case sourceURI :
527+ sources = append (sources , b .URI )
528+ case sourceBody :
529+ if hasBody {
530+ sources = append (sources , b .Body )
531+ }
532+ case sourceQuery :
533+ sources = append (sources , b .Query )
534+ case sourceHeader :
535+ sources = append (sources , b .Header )
536+ case sourceCookie :
537+ sources = append (sources , b .Cookie )
538+ }
539+ }
540+ } else {
541+ // Precedence: URL Params -> Body -> Query -> Headers -> Cookies
542+ sources = append (sources , b .URI )
543+
544+ // Check if both Body and Content-Type are set
545+ if hasBody {
546+ sources = append (sources , b .Body )
547+ }
548+ sources = append (sources , b .Query , b .Header , b .Cookie )
448549 }
449- sources = append ( sources , b . Query , b . Header , b . Cookie )
550+
450551 prevSkip := b .shouldSkipValidation
451552 b .shouldSkipValidation = true
452553
453- // TODO: Support custom precedence with an optional binding_source tag
454554 // TODO: Create WithOverrideEmptyValues
455555 // Bind from each source, but only update unset fields
456556 for _ , bindFunc := range sources {
0 commit comments