diff --git a/app.go b/app.go index e15383a63e..ca598fce9e 100644 --- a/app.go +++ b/app.go @@ -390,6 +390,13 @@ 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 + // + // 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 901b51744c..97c83d1598 100644 --- a/ctx.go +++ b/ctx.go @@ -335,7 +335,7 @@ func (c *Ctx) BodyParser(out interface{}) error { k, err = parseParamSquareBrackets(k) } - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { + if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { values := strings.Split(v, ",") for i := 0; i < len(values); i++ { data[k] = append(data[k], values[i]) @@ -1159,7 +1159,7 @@ func (c *Ctx) QueryParser(out interface{}) error { k, err = parseParamSquareBrackets(k) } - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { + if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { values := strings.Split(v, ",") for i := 0; i < len(values); i++ { data[k] = append(data[k], values[i]) @@ -1208,7 +1208,7 @@ 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) { + if c.app.config.EnableSplittingOnParsers && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { values := strings.Split(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 38e83ca9e2..a34778dec6 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 { @@ -3988,6 +3988,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 +4169,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 { @@ -4215,6 +4238,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 +4490,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 +4516,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/docs/api/fiber.md b/docs/api/fiber.md index fa0ad2412d..1f9a91b8bf 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -40,7 +40,7 @@ app := fiber.New(fiber.Config{ **Config fields** | Property | Type | Description | Default | -| ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | +| ---------------------------- | --------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --------------------- | | AppName | `string` | This allows to setup app name for the app | `""` | | BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | | CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | @@ -56,6 +56,7 @@ app := fiber.New(fiber.Config{ | ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | | EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma seperated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | | EnablePrintRoutes | `bool` | EnablePrintRoutes enables print all routes with their method, path, name and handler.. | `false` | +| EnableSplittingOnParsers | `bool` | 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` | `false` | | EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | | ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | | GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | @@ -63,13 +64,13 @@ app := fiber.New(fiber.Config{ | Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | | JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | | JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | -| Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | +| Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | | PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | | Prefork | `bool` | Enables use of the[`SO_REUSEPORT`](https://lwn.net/Articles/542629/)socket option. This will spawn multiple Go processes listening on the same port. learn more about [socket sharding](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/). **NOTE: if enabled, the application will need to be ran through a shell because prefork mode sets environment variables. If you're using Docker, make sure the app is ran with `CMD ./app` or `CMD ["sh", "-c", "/app"]`. For more info, see** [**this**](https://github.com/gofiber/fiber/issues/1021#issuecomment-730537971) **issue comment.** | `false` | | ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | | ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | | ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | -| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | +| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | | ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | | StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger then the current limit. | `false` | | StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | diff --git a/path_test.go b/path_test.go index 65dfdd6f72..d0d11d07a2 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