Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ v3 (feature): request binding #2006

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2cb58a2
remove old binding
trim21 Aug 23, 2022
3251afc
add new bind
trim21 Aug 23, 2022
a6696e5
replace panic with returning error
trim21 Aug 31, 2022
b5eeaa4
get typeID like stdlilb reflect
trim21 Aug 31, 2022
ffc1c41
support form and multipart
trim21 Aug 31, 2022
9887ac5
move internal/reflectunsafe into internal/bind
trim21 Aug 31, 2022
c8bc2e4
make content-type checking optional
trim21 Aug 31, 2022
257e791
add doc about chaining API
trim21 Aug 31, 2022
0be435a
Merge remote-tracking branch 'upstream/v3-beta' into bind
trim21 Sep 11, 2022
5569568
no alloc req headers logger
trim21 Sep 11, 2022
d8d0e52
handle error
trim21 Sep 11, 2022
df19a9e
lint
trim21 Sep 11, 2022
2369002
bench params
trim21 Sep 22, 2022
0883994
remove dead code
trim21 Sep 22, 2022
4ffac50
add more doc
trim21 Sep 22, 2022
f64b9d4
Merge remote-tracking branch 'upstream/v3-beta' into bind
trim21 Sep 23, 2022
befee12
fix test
trim21 Sep 23, 2022
d52652e
Merge remote-tracking branch 'origin/v3-beta' into bind
efectn Nov 15, 2022
6cb876a
add basic nested binding support (not yet for slices)
efectn Nov 17, 2022
3661d33
add support for queries like data[0][name] (not yet supporting deeper…
efectn Nov 20, 2022
4183069
support pointer fields
efectn Nov 27, 2022
7345517
add old methods
efectn Dec 14, 2022
081809e
feat: support float
FGYFFFF Jan 5, 2023
21c6e40
Merge remote-tracking branch 'upstream/v3-beta' into bind
trim21 Jan 11, 2023
967e52a
Merge remote-tracking branch 'origin/v3-beta' into bind
efectn Aug 6, 2023
8e6b3bb
fix mws
efectn Aug 6, 2023
3c20e85
Merge remote-tracking branch 'origin/main' into bind
efectn Mar 16, 2024
8a77269
update somem methods
efectn Mar 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 39 additions & 24 deletions app.go
Expand Up @@ -10,6 +10,8 @@ package fiber
import (
"bufio"
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net"
Expand All @@ -22,9 +24,6 @@ import (
"sync/atomic"
"time"

"encoding/json"
"encoding/xml"

"github.com/gofiber/fiber/v3/utils"
"github.com/valyala/fasthttp"
)
Expand Down Expand Up @@ -116,10 +115,14 @@ type App struct {
latestGroup *Group
// newCtxFunc
newCtxFunc func(app *App) CustomCtx
// custom binders
customBinders []CustomBinder
// TLS handler
tlsHandler *TLSHandler
// bind decoder cache
bindDecoderCache sync.Map
// form decoder cache
formDecoderCache sync.Map
// multipart decoder cache
multipartDecoderCache sync.Map
}

// Config is a struct holding the server settings.
Expand Down Expand Up @@ -322,6 +325,23 @@ type Config struct {
// Default: xml.Marshal
XMLEncoder utils.XMLMarshal `json:"-"`

// XMLDecoder set by an external client of Fiber it will use the provided implementation of a
// XMLUnmarshal
//
// Allowing for flexibility in using another XML library for encoding
// Default: utils.XMLUnmarshal
XMLDecoder utils.XMLUnmarshal `json:"-"`

// App validate. if nil, and context.EnableValidate will always return a error.
// Default: nil
Validator Validator

// Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)
// WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chose.
//
// Default: NetworkTCP4
Network string

// If you find yourself behind some sort of proxy, like a load balancer,
// then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header.
// For example, the Host HTTP header is usually used to return the requested host.
Expand Down Expand Up @@ -362,12 +382,6 @@ type Config struct {
//
// Optional. Default: DefaultColors
ColorScheme Colors `json:"color_scheme"`

// If you want to validate header/form/query... automatically when to bind, you can define struct validator.
// Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator.
//
// Default: nil
StructValidator StructValidator
}

// Static defines configuration options when defining static assets.
Expand Down Expand Up @@ -456,13 +470,12 @@ func New(config ...Config) *App {
stack: make([][]*Route, len(intMethod)),
treeStack: make([]map[string][]*Route, len(intMethod)),
// Create config
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
appList: make(map[string]*App),
latestRoute: &Route{},
latestGroup: &Group{},
customBinders: []CustomBinder{},
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
appList: make(map[string]*App),
latestRoute: &Route{},
latestGroup: &Group{},
}

// Create Ctx pool
Expand Down Expand Up @@ -510,9 +523,17 @@ func New(config ...Config) *App {
if app.config.JSONDecoder == nil {
app.config.JSONDecoder = json.Unmarshal
}

if app.config.XMLEncoder == nil {
app.config.XMLEncoder = xml.Marshal
}
if app.config.XMLDecoder == nil {
app.config.XMLDecoder = xml.Unmarshal
}

if app.config.Network == "" {
app.config.Network = NetworkTCP4
}

app.config.trustedProxiesMap = make(map[string]struct{}, len(app.config.TrustedProxies))
for _, ipAddress := range app.config.TrustedProxies {
Expand Down Expand Up @@ -553,12 +574,6 @@ func (app *App) NewCtxFunc(function func(app *App) CustomCtx) {
app.newCtxFunc = function
}

// You can register custom binders to use as Bind().Custom("name").
// They should be compatible with CustomBinder interface.
func (app *App) RegisterCustomBinder(binder CustomBinder) {
app.customBinders = append(app.customBinders, binder)
}

// You can use SetTLSHandler to use ClientHelloInfo when using TLS with Listener.
func (app *App) SetTLSHandler(tlsHandler *TLSHandler) {
// Attach the tlsHandler to the config
Expand Down
205 changes: 35 additions & 170 deletions bind.go
@@ -1,194 +1,59 @@
package fiber

import (
"github.com/gofiber/fiber/v3/binder"
"github.com/gofiber/fiber/v3/utils"
)

// An interface to register custom binders.
type CustomBinder interface {
Name() string
MIMETypes() []string
Parse(Ctx, any) error
}

// An interface to register custom struct validator for binding.
type StructValidator interface {
Engine() any
ValidateStruct(any) error
}

// Bind struct
type Bind struct {
ctx *DefaultCtx
should bool
}

// To handle binder errors manually, you can prefer Should method.
// It's default behavior of binder.
func (b *Bind) Should() *Bind {
b.should = true

return b
}

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

return b
}

// Check Should/Must 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())
}

return err
}

// Struct validation.
func (b *Bind) validateStruct(out any) error {
validator := b.ctx.app.config.StructValidator
if validator != nil {
return validator.ValidateStruct(out)
}

return nil
}

// 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.
func (b *Bind) Custom(name string, dest any) error {
binders := b.ctx.App().customBinders
for _, binder := range binders {
if binder.Name() == name {
return b.returnErr(binder.Parse(b.ctx, dest))
}
}

return ErrCustomBinderNotFound
}

// Header binds the request header strings into the struct, map[string]string and map[string][]string.
func (b *Bind) Header(out any) error {
if err := b.returnErr(binder.HeaderBinder.Bind(b.ctx.Request(), out)); err != nil {
return err
}

return b.validateStruct(out)
}

// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string.
func (b *Bind) RespHeader(out any) error {
if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil {
return err
}
"fmt"
"reflect"

return b.validateStruct(out)
}

// Cookie binds the requesr 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.Context(), out)); err != nil {
return err
}

return b.validateStruct(out)
}

// QueryParser binds the query string into the struct, map[string]string and map[string][]string.
func (b *Bind) Query(out any) error {
if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.Context(), out)); err != nil {
return err
}
"github.com/gofiber/fiber/v3/internal/bind"
)

return b.validateStruct(out)
type Binder interface {
UnmarshalFiberCtx(ctx Ctx) error
}

// JSON binds the body string into the struct.
func (b *Bind) JSON(out any) error {
if err := b.returnErr(binder.JSONBinder.Bind(b.ctx.Body(), b.ctx.App().Config().JSONDecoder, out)); err != nil {
return err
}

return b.validateStruct(out)
// decoder should set a field on reqValue
// it's created with field index
type decoder interface {
Decode(ctx Ctx, reqValue reflect.Value) error
}

// XML binds the body string into the struct.
func (b *Bind) XML(out any) error {
if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil {
return err
}

return b.validateStruct(out)
type fieldCtxDecoder struct {
index int
fieldName string
fieldType reflect.Type
}

// Form binds the form into the struct, map[string]string and map[string][]string.
func (b *Bind) Form(out any) error {
if err := b.returnErr(binder.FormBinder.Bind(b.ctx.Context(), out)); err != nil {
return err
}
func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
v := reflect.New(d.fieldType)
unmarshaler := v.Interface().(Binder)

return b.validateStruct(out)
}

// URI binds the route parameters into the struct, map[string]string and map[string][]string.
func (b *Bind) URI(out any) error {
if err := b.returnErr(binder.URIBinder.Bind(b.ctx.route.Params, b.ctx.Params, out)); err != nil {
if err := unmarshaler.UnmarshalFiberCtx(ctx); err != nil {
return err
}

return b.validateStruct(out)
reqValue.Field(d.index).Set(v.Elem())
return nil
}

// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string.
func (b *Bind) MultipartForm(out any) error {
if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.Context(), out)); err != nil {
return err
}

return b.validateStruct(out)
type fieldTextDecoder struct {
index int
fieldName string
tag string // query,param,header,respHeader ...
reqField string
dec bind.TextDecoder
get func(c Ctx, key string, defaultValue ...string) string
}

// Body binds the request body into the struct, map[string]string and map[string][]string.
// It supports decoding the following content types based on the Content-Type header:
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
// If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder.
// If there're no custom binder for mşme type of body, it will return a ErrUnprocessableEntity error.
func (b *Bind) Body(out any) error {
// Get content-type
ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType()))
ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype))

// Parse body accordingly
switch ctype {
case MIMEApplicationJSON:
return b.JSON(out)
case MIMETextXML, MIMEApplicationXML:
return b.XML(out)
case MIMEApplicationForm:
return b.Form(out)
case MIMEMultipartForm:
return b.MultipartForm(out)
func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
text := d.get(ctx, d.reqField)
if text == "" {
return nil
}

// Check custom binders
binders := b.ctx.App().customBinders
for _, binder := range binders {
for _, mime := range binder.MIMETypes() {
if mime == ctype {
return b.returnErr(binder.Parse(b.ctx, out))
}
}
err := d.dec.UnmarshalString(text, reqValue.Field(d.index))
if err != nil {
return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqField, err)
}

// No suitable content type found
return ErrUnprocessableEntity
return nil
}