Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions net/ghttp/ghttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,13 @@ type (

// handlerFuncInfo contains the HandlerFunc address and its reflection type.
handlerFuncInfo struct {
Func HandlerFunc // Handler function address.
Type reflect.Type // Reflect type information for current handler, which is used for extensions of the handler feature.
Value reflect.Value // Reflect value information for current handler, which is used for extensions of the handler feature.
IsStrictRoute bool // Whether strict route matching is enabled.
ReqStructFields []gstructs.Field // Request struct fields.
Func HandlerFunc // Handler function address.
Type reflect.Type // Reflect type information for current handler, which is used for extensions of the handler feature.
Value reflect.Value // Reflect value information for current handler, which is used for extensions of the handler feature.
IsStrictRoute bool // Whether strict route matching is enabled.
ReqStructFields []gstructs.Field // Request struct fields.
ReqBodyFieldName string // Request struct field name tagged with `in:"body"`, which receives the whole request body.
ReqBodyFieldTagName string // Tag name of the `in:"body"` field, which is resolved once at router registering for the request handling to remove the parameter of the same name.
}

// HandlerItem is the registered handler for route handling,
Expand Down
1 change: 1 addition & 0 deletions net/ghttp/ghttp_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Request struct {
queryMap map[string]any // Query parameters map, which is nil if there's no query string.
formMap map[string]any // Form parameters map, which is nil if there's no form of data from the client.
bodyMap map[string]any // Body parameters map, which might be nil if their nobody content.
bodyArray []any // Body parameters array, which is nil unless the request body is a JSON array for a handler that declares a field tagged with `in:"body"`.
error error // Current executing error of the request.
exitAll bool // A bool marking whether current request is exited.
parsedHost string // The parsed host name for current host used by GetHost function.
Expand Down
42 changes: 42 additions & 0 deletions net/ghttp/ghttp_request_param.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ func (r *Request) parseBody() {
return
}
r.parsedBody = true
// The body might be re-parsed after being changed, for example the middleware calling
// ReloadParam, so the previously parsed results are reset to avoid the stale ones being
// mixed up between the object and array formats.
r.bodyMap = nil
r.bodyArray = nil
// There's no data posted.
if r.ContentLength == 0 {
return
Expand All @@ -243,6 +248,20 @@ func (r *Request) parseBody() {
jsonContentType := gstr.ContainsI(contentType, contentTypeJson)
// Preserve GET query/form body compatibility while validating JSON-shaped GET bodies.
strictJsonContentType := jsonContentType && (r.Method != http.MethodGet || body[0] == '{' || body[0] == '[')
// JSON array format check for the request struct declaring a field tagged with `in:"body"`.
// Note that the array body is accepted no matter what the content type is, just like the
// object body is relaxed checked below.
if r.isArrayRequestBodyExpected() && body[0] == '[' && body[len(body)-1] == ']' {
var array []any
if err := json.UnmarshalUseNumber(body, &array); err == nil {
r.bodyArray = array
return
} else if strictJsonContentType {
r.SetError(gerror.WrapCode(gcode.CodeInvalidParameter, err, "Parse JSON body failed"))
return
}
// It is not a valid JSON array, falling back to the default parameters decoding below.
}
// JSON format checks.
if strictJsonContentType {
if err := json.UnmarshalUseNumber(body, &r.bodyMap); err != nil {
Expand All @@ -266,6 +285,19 @@ func (r *Request) parseBody() {
}
}

// isArrayRequestBodyExpected checks and returns whether the handler serving current request
// declares a request struct field tagged with `in:"body"`, which receives the whole JSON array
// request body.
//
// Note that there might be no serving handler for current request, for example the static file
// request or the route not matched request.
func (r *Request) isArrayRequestBodyExpected() bool {
if r.serveHandler == nil || r.serveHandler.Handler == nil {
return false
}
return r.serveHandler.Handler.Info.ReqBodyFieldName != ""
}

// parseForm parses the request form for HTTP method PUT, POST, PATCH.
// The form data is pared into r.formMap.
//
Expand All @@ -289,6 +321,16 @@ func (r *Request) parseForm() {
// To avoid big memory consuming.
// The `multipart/` type form always contains binary data, which is not necessary read twice.
r.MakeBodyRepeatableRead(true)
// A field tagged with `in:"body"` receives the whole request body, so the JSON array
// body is detected before the form decoding below, which would otherwise cut the
// array into bogus form parameters.
if r.isArrayRequestBodyExpected() {
r.parseBody()
if r.bodyArray != nil {
r.formMap = nil
return
}
}
}
if isMultiPartRequest {
// multipart/form-data, multipart/mixed
Expand Down
32 changes: 32 additions & 0 deletions net/ghttp/ghttp_request_param_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package ghttp

import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/goai"
"github.com/gogf/gf/v2/os/gstructs"
"github.com/gogf/gf/v2/util/gconv"
Expand Down Expand Up @@ -180,6 +182,14 @@ func (r *Request) doGetRequestStruct(pointer any, mapping ...map[string]string)
if data == nil {
data = map[string]any{}
}
// A field tagged with `in:"body"` represents the complete request body. Before converting,
// remove the request parameter that is named exactly after the field tag name, as the tag
// name is matched with a higher priority than the field name by the struct converting,
// which would otherwise overwrite the body array. Both names are resolved at router
// registering time, see checkAndCreateReqBodyField.
if r.serveHandler.Handler.Info.ReqBodyFieldName != "" {
delete(data, r.serveHandler.Handler.Info.ReqBodyFieldTagName)
}

// `in` Tag Struct values.
if err = r.mergeInTagStructValue(data); err != nil {
Expand All @@ -191,6 +201,28 @@ func (r *Request) doGetRequestStruct(pointer any, mapping ...map[string]string)
return data, nil
}

// The request struct field tagged with `in:"body"` receives the whole request body, which
// is a JSON array instead of being split into the request parameters.
if bodyFieldName := r.serveHandler.Handler.Info.ReqBodyFieldName; bodyFieldName != "" {
if r.bodyArray != nil {
data[bodyFieldName] = r.bodyArray
} else if r.bodyMap != nil || r.MultipartForm != nil {
// The JSON object, the form parameters and the multipart forms are not acceptable
// for such field, which are reported as an invalid parameter instead of being
// silently ignored with the field left as a nil slice.
return nil, gerror.NewCodef(
gcode.CodeInvalidParameter,
`the request body should be a JSON array for the request struct field "%s" tagged with in:"body"`,
bodyFieldName,
)
} else {
// There's no body at all. The nil value occupies the field name, so that the field
// is bound to its zero value before the request parameters of similar names (case
// or symbol variants) could be fuzzy matched to it.
data[bodyFieldName] = nil
}
}

return data, gconv.Struct(data, pointer, mapping...)
}

Expand Down
57 changes: 57 additions & 0 deletions net/ghttp/ghttp_server_service_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/goai"
"github.com/gogf/gf/v2/os/gstructs"
"github.com/gogf/gf/v2/text/gstr"
)
Expand Down Expand Up @@ -235,10 +236,66 @@ func (s *Server) checkAndCreateFuncInfo(
return funcInfo, err
}
funcInfo.ReqStructFields = fields
if err = funcInfo.checkAndCreateReqBodyField(); err != nil {
return funcInfo, err
}
funcInfo.Func = createRouterFunc(funcInfo)
return
}

// checkAndCreateReqBodyField retrieves the request struct field that receives the whole request
// body, which is declared by the `in:"body"` tag on the field.
//
// The request body is only able to be received by one field, so it returns error if there are
// multiple fields tagged with `in:"body"`. It also returns error if the field is not of slice
// type, as only the JSON array request body is supported for now; note that an object request
// body does not need this tag, which is received by the ordinary request struct fields.
//
// It is called once at handler registration, which also resolves the tag name of the field for
// the request handling to remove the request parameter of the same name.
func (f *handlerFuncInfo) checkAndCreateReqBodyField() error {
for _, field := range f.ReqStructFields {
if field.TagIn() != goai.ParameterInBody {
continue
}
if f.ReqBodyFieldName != "" {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid handler: only one request struct field is allowed to be tagged with in:"body", `+
`but got both "%s" and "%s"`,
f.ReqBodyFieldName, field.Name(),
)
}
var fieldType = field.Type().Type
for fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if fieldType.Kind() != reflect.Slice {
var hint string
switch fieldType.Kind() {
case reflect.Array:
hint = `; a fixed-size array can be replaced by a slice with a length validation rule`
case reflect.Struct, reflect.Map, reflect.Interface:
hint = `; an object request body is received by the ordinary fields of the request ` +
`struct, which does not need the in:"body" tag`
default:
}
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid handler: the request struct field "%s" tagged with in:"body" should be type of `+
`slice, but got "%s"%s`,
field.Name(), field.Type().String(), hint,
)
}
f.ReqBodyFieldName = field.Name()
// The tag name is also resolved here, as the request handling removes the request
// parameter of the same name before the struct converting, which is matched with a
// higher priority than the field name by the struct converting.
f.ReqBodyFieldTagName = field.TagPriorityName()
}
return nil
}

func createRouterFunc(funcInfo handlerFuncInfo) func(r *Request) {
return func(r *Request) {
var (
Expand Down
Loading
Loading