diff --git a/app.go b/app.go index e15383a63e4..8a2d9f1e723 100644 --- a/app.go +++ b/app.go @@ -390,6 +390,15 @@ type Config struct { // // Optional. Default: DefaultMethods RequestMethods []string + + // EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true. + // For example, you can use it to parse multiple values from a query parameter like this: + // /api?foo=bar,baz == foo[]=bar&foo[]=baz + // If there is an escape character before the comma, parsers won't split it like the example below: + // /api?foo=bar\,baz == foo=bar,baz + // + // Optional. Default: false + EnableSplittingOnParsers bool `json:"enable_splitting_on_parsers"` } // Static defines configuration options when defining static assets. diff --git a/ctx.go b/ctx.go index 901b51744c6..30a5c0c8039 100644 --- a/ctx.go +++ b/ctx.go @@ -335,8 +335,8 @@ func (c *Ctx) BodyParser(out interface{}) error { k, err = parseParamSquareBrackets(k) } - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") + if findNextNonEscapedCharsetPosition(v, []byte(",")) != -1 && equalFieldType(out, reflect.Slice, k) && c.app.config.EnableSplittingOnParsers { + values := splitNonEscaped(v, ",") for i := 0; i < len(values); i++ { data[k] = append(data[k], values[i]) } @@ -1159,10 +1159,10 @@ func (c *Ctx) QueryParser(out interface{}) error { k, err = parseParamSquareBrackets(k) } - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") + if findNextNonEscapedCharsetPosition(v, []byte(",")) != -1 && equalFieldType(out, reflect.Slice, k) && c.app.config.EnableSplittingOnParsers { + values := splitNonEscaped(v, ",") for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) + data[k] = append(data[k], RemoveEscapeChar(values[i])) } } else { data[k] = append(data[k], v) @@ -1208,8 +1208,8 @@ func (c *Ctx) ReqHeaderParser(out interface{}) error { k := c.app.getString(key) v := c.app.getString(val) - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") + if findNextNonEscapedCharsetPosition(v, []byte(",")) != -1 && equalFieldType(out, reflect.Slice, k) && c.app.config.EnableSplittingOnParsers { + values := splitNonEscaped(v, ",") for i := 0; i < len(values); i++ { data[k] = append(data[k], values[i]) } diff --git a/ctx_test.go b/ctx_test.go index 38e83ca9e22..87b57c90ba7 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -3917,7 +3917,7 @@ func Benchmark_Ctx_Queries(b *testing.B) { // go test -run Test_Ctx_QueryParser -v func Test_Ctx_QueryParser(t *testing.T) { t.Parallel() - app := New() + app := New(Config{EnableSplittingOnParsers: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) type Query struct { @@ -3937,6 +3937,11 @@ func Test_Ctx_QueryParser(t *testing.T) { utils.AssertEqual(t, nil, c.QueryParser(q)) utils.AssertEqual(t, 2, len(q.Hobby)) + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball\\,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") q = new(Query) utils.AssertEqual(t, nil, c.QueryParser(q)) @@ -3988,6 +3993,29 @@ func Test_Ctx_QueryParser(t *testing.T) { utils.AssertEqual(t, 2, len(aq.Data)) } +// go test -run Test_Ctx_QueryParser -v +func Test_Ctx_QueryParser_WithoutSplitting(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + ID int + Name string + Hobby []string + } + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q := new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) +} + // go test -run Test_Ctx_QueryParser_WithSetParserDecoder -v func Test_Ctx_QueryParser_WithSetParserDecoder(t *testing.T) { t.Parallel() @@ -4146,7 +4174,7 @@ func Test_Ctx_QueryParser_Schema(t *testing.T) { // go test -run Test_Ctx_ReqHeaderParser -v func Test_Ctx_ReqHeaderParser(t *testing.T) { t.Parallel() - app := New() + app := New(Config{EnableSplittingOnParsers: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) type Header struct { @@ -4170,6 +4198,13 @@ func Test_Ctx_ReqHeaderParser(t *testing.T) { utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) utils.AssertEqual(t, 3, len(q.Hobby)) + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang\\,fiber\\,go,fiber") + q = new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + fmt.Println(q.Hobby) + utils.AssertEqual(t, 2, len(q.Hobby)) + empty := new(Header) c.Request().Header.Del("hobby") utils.AssertEqual(t, nil, c.QueryParser(empty)) @@ -4215,6 +4250,34 @@ func Test_Ctx_ReqHeaderParser(t *testing.T) { utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) } +// go test -run Test_Ctx_ReqHeaderParser -v +func Test_Ctx_ReqHeaderParser_WithoutSplitting(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + q := new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) + + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang,fiber,go") + q = new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) +} + // go test -run Test_Ctx_ReqHeaderParser_WithSetParserDecoder -v func Test_Ctx_ReqHeaderParser_WithSetParserDecoder(t *testing.T) { t.Parallel() @@ -4439,7 +4502,7 @@ func Benchmark_Ctx_parseQuery(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser_Comma -benchmem -count=4 func Benchmark_Ctx_QueryParser_Comma(b *testing.B) { - app := New() + app := New(Config{EnableSplittingOnParsers: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) type Query struct { @@ -4465,7 +4528,7 @@ func Benchmark_Ctx_QueryParser_Comma(b *testing.B) { // go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParser -benchmem -count=4 func Benchmark_Ctx_ReqHeaderParser(b *testing.B) { - app := New() + app := New(Config{EnableSplittingOnParsers: true}) c := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(c) type ReqHeader struct { diff --git a/path_test.go b/path_test.go index 65dfdd6f726..d0d11d07a2e 100644 --- a/path_test.go +++ b/path_test.go @@ -195,6 +195,17 @@ func Test_Utils_RemoveEscapeChar(t *testing.T) { utils.AssertEqual(t, "noEscapeChar", res) } +func Benchmark_Utils_RemoveEscapeChar(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + var res string + for n := 0; n < b.N; n++ { + res = RemoveEscapeChar(":test\\:bla") + } + + utils.AssertEqual(b, ":test:bla", res) +} + // go test -race -run Test_Path_matchParams func Benchmark_Path_matchParams(t *testing.B) { var ctxParams [maxParams]string