From f6ac929fde9c5502d2b80abc0fe679d5e6fe5dc3 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Thu, 20 Mar 2025 06:35:59 -0700 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20Mark=20unused=20tes?= =?UTF-8?q?ts=20with=20t.SkipNow=20(#3366)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚨 test: uncomment test and mark it with t.SkipNow * fix lint issues --- middleware/csrf/csrf_test.go | 91 ++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 090082f4..7f586cd9 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -1331,56 +1331,65 @@ func Test_CSRF_Cookie_Injection_Exploit(t *testing.T) { } // TODO: use this test case and make the unsafe header value bug from https://github.com/gofiber/fiber/issues/2045 reproducible and permanently fixed/tested by this testcase -// func Test_CSRF_UnsafeHeaderValue(t *testing.T) { -// t.Parallel() -// app := fiber.New() +func Test_CSRF_UnsafeHeaderValue(t *testing.T) { + t.SkipNow() + t.Parallel() + app := fiber.New() -// app.Use(New()) -// app.Get("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Get("/test", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Post("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) + app.Use(New()) + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + app.Get("/test", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + app.Post("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) -// resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) -// require.NoError(t, err) -// require.Equal(t, fiber.StatusOK, resp.StatusCode) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// var token string -// for _, c := range resp.Cookies() { -// if c.Name != ConfigDefault.CookieName { -// continue -// } -// token = c.Value -// break -// } + var token string + for _, c := range resp.Cookies() { + if c.Name != ConfigDefault.CookieName { + continue + } + token = c.Value + break + } -// fmt.Println("token", token) + t.Log("token", token) -// getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) -// getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) + getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + getReq.Header.Set(HeaderName, token) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) -// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// getReq.Header.Set(fiber.HeaderCacheControl, "no") -// getReq.Header.Set(HeaderName, token) + getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) + getReq.Header.Set("X-Requested-With", "XMLHttpRequest") + getReq.Header.Set(fiber.HeaderCacheControl, "no") + getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// getReq.Header.Set(fiber.HeaderAccept, "*/*") -// getReq.Header.Del(HeaderName) -// resp, err = app.Test(getReq) + getReq.Header.Set(fiber.HeaderAccept, "*/*") + getReq.Header.Del(HeaderName) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) -// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// postReq.Header.Set(HeaderName, token) -// resp, err = app.Test(postReq) -// } + postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) + postReq.Header.Set("X-Requested-With", "XMLHttpRequest") + postReq.Header.Set(HeaderName, token) + resp, err = app.Test(postReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) +} // go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4 func Benchmark_Middleware_CSRF_Check(b *testing.B) { From e947e03ed2d072b683aa69757109dee8cf565c08 Mon Sep 17 00:00:00 2001 From: Edvard <75655486+edvardsanta@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:13:21 -0300 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=94=A5=20feat(logger):=20Add=20predef?= =?UTF-8?q?ined=20log=20formats=20(#3359)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(logger): Add predefined log formats This commit introduces predefined log formats for the logger middleware, enhancing its flexibility and ease of use. Users can now specify formats like "common", "combined", and "json" in addition to the default format. Changes: - Added a `format.go` file to store predefined log format constants. - Updated `config.go` to include documentation for the `Format` configuration option, explaining the available placeholders and predefined formats. - Modified `logger.go` to utilize the predefined formats based on the `Format` configuration. - Added a new test case `Test_Logger_CLF` in `logger_test.go` to verify the "common" log format. * feat(logger): Use predefined formats and fix default format This commit updates the logger middleware to utilize the predefined log formats introduced in a previous commit. It also fixes the default format to use the `FormatDefault` constant. Changes: - Updated `config.go` to use `FormatDefault` constant for the default format. - Updated `default_logger.go` to use `FormatDefault` constant for the default format. - Added new test cases in `logger_test.go` to verify the "common", "combined" and "json" log formats. - Updated `format.go` to add newline character to the end of the default format. * feat(logger): Document and exemplify predefined formats * fix(logger): Improve test assertions based on golangci-lint * docs(logger): Improve documentation and formatting logger.md based on markdownlint-cli2 * docs(logger): Improve documentation based on markdownlint-cli2 * fix(logger): Improve combined and JSON format tests * feat(logger): Add ECS log format * feat(logger): Add CustomFormat option This commit introduces a `CustomFormat` option to the `Config` struct, allowing users to specify a predefined format (like "common", "combined", "json", or "ecs") * feat(logger): Add ECS log format to examples and config * docs(logger): Update examples in whats_new.md * feat(logger): Remove CustomFormat option and renamed Format consts - Removed `CustomFormat` field from `Config`. - Removed `LoggerConfig` map. - Rename predefined formats constants. * docs(logger): Update documentation and examples after format refactor --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- docs/middleware/logger.md | 78 ++++++++++++------ docs/whats_new.md | 16 ++++ middleware/logger/config.go | 23 ++++-- middleware/logger/default_logger.go | 2 +- middleware/logger/format.go | 14 ++++ middleware/logger/logger.go | 1 - middleware/logger/logger_test.go | 118 ++++++++++++++++++++++++++++ 7 files changed, 220 insertions(+), 32 deletions(-) create mode 100644 middleware/logger/format.go diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index af16f384..c4c60cbc 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -79,7 +79,7 @@ app.Use(logger.New(logger.Config{ TimeZone: "Asia/Shanghai", Done: func(c fiber.Ctx, logString []byte) { if c.Response().StatusCode() != fiber.StatusOK { - reporter.SendToSlack(logString) + reporter.SendToSlack(logString) } }, })) @@ -88,6 +88,23 @@ app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{ DisableColors: true, })) + +// Use predefined formats +app.Use(logger.New(logger.Config{ + Format: logger.FormatCommon, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatCombined, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatJSON, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatECS, +})) ``` ### Use Logger Middleware with Other Loggers @@ -136,37 +153,50 @@ Writing to os.File is goroutine-safe, but if you are using a custom Stream that ### Config -| Property | Type | Description | Default | -|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------| -| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` | -| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` | -| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | -| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` | -| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | -| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | -| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | -| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` | -| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | -| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | +| Property | Type | Description | Default | +| :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | +| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` | +| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` | +| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | +| `Format` | `string` | Defines the logging tags. See more in [Predefined Formats](#predefined-formats), or create your own using [Tags](#constants). | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` (same as `DefaultFormat`) | +| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | +| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | +| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | +| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` | +| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | +| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | ## Default Config ```go var ConfigDefault = Config{ - Next: nil, - Skip nil, - Done: nil, - Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n", - TimeFormat: "15:04:05", - TimeZone: "Local", - TimeInterval: 500 * time.Millisecond, - Stream: os.Stdout, - DisableColors: false, - LoggerFunc: defaultLoggerInstance, + Next: nil, + Skip: nil, + Done: nil, + Format: DefaultFormat, + TimeFormat: "15:04:05", + TimeZone: "Local", + TimeInterval: 500 * time.Millisecond, + Stream: os.Stdout, + BeforeHandlerFunc: beforeHandlerFunc, + LoggerFunc: defaultLoggerInstance, + enableColors: true, } ``` +## Predefined Formats + +Logger provides predefined formats that you can use by name or directly by specifying the format string. + +| **Format Constant** | **Format String** | **Description** | +|---------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| `DefaultFormat` | `"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"` | Fiber's default logger format. | +| `CommonFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent}\n"` | Common Log Format (CLF) used in web server logs. | +| `CombinedFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent} "${referer}" "${ua}"\n"` | CLF format plus the `referer` and `user agent` fields. | +| `JSONFormat` | `"{time: ${time}, ip: ${ip}, method: ${method}, url: ${url}, status: ${status}, bytesSent: ${bytesSent}}\n"` | JSON format for structured logging. | +| `ECSFormat` | `"{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n"` | Elastic Common Schema (ECS) format for structured logging. | + ## Constants ```go diff --git a/docs/whats_new.md b/docs/whats_new.md index 8528dc41..bc569d3e 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -937,6 +937,22 @@ app.Use(logger.New(logger.Config{ +#### Predefined Formats + +Logger provides predefined formats that you can use by name or directly by specifying the format string. +
+ +Example Usage + +```go +app.Use(logger.New(logger.Config{ + Format: logger.FormatCombined, +})) +``` + +See more in [Logger](./middleware/logger.md#predefined-formats) +
+ ### Filesystem We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 2df814eb..d543acfa 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -50,9 +50,23 @@ type Config struct { timeZoneLocation *time.Location - // Format defines the logging tags + // Format defines the logging format for the middleware. // - // Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error} + // You can customize the log output by defining a format string with placeholders + // such as: ${time}, ${ip}, ${status}, ${method}, ${path}, ${latency}, ${error}, etc. + // The full list of available placeholders can be found in 'tags.go' or at + // 'https://docs.gofiber.io/api/middleware/logger/#constants'. + // + // Fiber provides predefined logging formats that can be used directly: + // + // - DefaultFormat → Uses the default log format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}" + // - CommonFormat → Uses the Apache Common Log Format (CLF): "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n" + // - CombinedFormat → Uses the Apache Combined Log Format: "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n" + // - JSONFormat → Uses the JSON log format: "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n" + // - ECSFormat → Uses the Elastic Common Schema (ECS) log format: {\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}" + // If both `Format` and `CustomFormat` are provided, the `CustomFormat` will be used, and the `Format` field will be ignored. + // If no format is specified, the default format is used: + // "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}" Format string // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html @@ -105,7 +119,7 @@ var ConfigDefault = Config{ Next: nil, Skip: nil, Done: nil, - Format: defaultFormat, + Format: DefaultFormat, TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, @@ -115,9 +129,6 @@ var ConfigDefault = Config{ enableColors: true, } -// default logging format for Fiber's default logger -var defaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n" - // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index c70a3e0e..a2cbfa1f 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -28,7 +28,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf := bytebufferpool.Get() // Default output when no custom Format or io.Writer is given - if cfg.Format == defaultFormat { + if cfg.Format == DefaultFormat { // Format error if exist formatErr := "" if cfg.enableColors { diff --git a/middleware/logger/format.go b/middleware/logger/format.go new file mode 100644 index 00000000..901c2409 --- /dev/null +++ b/middleware/logger/format.go @@ -0,0 +1,14 @@ +package logger + +const ( + // Fiber's default logger + DefaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n" + // Apache Common Log Format (CLF) + CommonFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n" + // Apache Combined Log Format + CombinedFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n" + // JSON log formats + JSONFormat = "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n" + // Elastic Common Schema (ECS) Log Format + ECSFormat = "{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n" +) diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 793c16c2..7d4befc9 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -40,7 +40,6 @@ func New(config ...Config) fiber.Handler { } }() } - // Set PID once pid := strconv.Itoa(os.Getpid()) diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index eb1b6a44..edce174c 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -467,6 +467,124 @@ func Test_Logger_All(t *testing.T) { require.Equal(t, expected, buf.String()) } +func Test_Logger_CLF_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: CommonFormat, + Stream: buf, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("0.0.0.0 - - [%s] \"%s %s %s\" %d %d\n", + time.Now().Format("15:04:05"), + fiber.MethodGet, "/?foo=bar", "HTTP/1.1", + fiber.StatusNotFound, + 0) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_Combined_CLF_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: CombinedFormat, + Stream: buf, + })) + const expectedUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" + const expectedReferer = "http://example.com" + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + req.Header.Set("Referer", expectedReferer) + req.Header.Set("User-Agent", expectedUA) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("0.0.0.0 - - [%s] %q %d %d %q %q\n", + time.Now().Format("15:04:05"), + fmt.Sprintf("%s %s %s", fiber.MethodGet, "/?foo=bar", "HTTP/1.1"), + fiber.StatusNotFound, + 0, + expectedReferer, + expectedUA) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_Json_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: JSONFormat, + Stream: buf, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf( + "{\"time\":%q,\"ip\":%q,\"method\":%q,\"url\":%q,\"status\":%d,\"bytesSent\":%d}\n", + time.Now().Format("15:04:05"), + "0.0.0.0", + fiber.MethodGet, + "/?foo=bar", + fiber.StatusNotFound, + 0, + ) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_ECS_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: ECSFormat, + Stream: buf, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf( + "{\"@timestamp\":%q,\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":%q},\"http\":{\"request\":{\"method\":%q,\"url\":%q,\"protocol\":%q},\"response\":{\"status_code\":%d,\"body\":{\"bytes\":%d}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":%q}\n", + time.Now().Format("15:04:05"), + "0.0.0.0", + fiber.MethodGet, + "/?foo=bar", + "HTTP/1.1", + fiber.StatusNotFound, + 0, + fmt.Sprintf("%s %s responded with %d", fiber.MethodGet, "/?foo=bar", fiber.StatusNotFound), + ) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + func getLatencyTimeUnits() []struct { unit string div time.Duration From ef40c04ede816d9ce2071e3a0034b3a706789520 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Sun, 23 Mar 2025 23:58:43 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20reduce=20D?= =?UTF-8?q?efaultCtx=20from=20768=20bytes=20to=20736=20bytes=20(#3353)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: reduce DefaultCtx from 768 bytes to 736 bytes * ♻️ Refactor: add comments --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- ctx.go | 69 +++++++++++++++++++++++++++---------------------------- router.go | 2 +- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/ctx.go b/ctx.go index b2d13c0a..efd6006d 100644 --- a/ctx.go +++ b/ctx.go @@ -49,28 +49,26 @@ const userContextKey contextKey = 0 // __local_user_context__ // //go:generate ifacemaker --file ctx.go --struct DefaultCtx --iface Ctx --pkg fiber --output ctx_interface_gen.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on." type DefaultCtx struct { - app *App // Reference to *App - route *Route // Reference to *Route - fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx - bind *Bind // Default bind reference - redirect *Redirect // Default redirect reference - req *DefaultReq // Default request api reference - res *DefaultRes // Default response api reference - values [maxParams]string // Route parameter values - viewBindMap sync.Map // Default view map to bind template engine - method string // HTTP method - baseURI string // HTTP base uri - path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer - detectionPath string // Route detection path -> string copy from detectionPathBuffer - treePath string // Path for the search in the tree - pathOriginal string // Original HTTP path - pathBuffer []byte // HTTP path buffer - detectionPathBuffer []byte // HTTP detectionPath buffer - flashMessages redirectionMsgs // Flash messages - indexRoute int // Index of the current route - indexHandler int // Index of the current handler - methodINT int // HTTP method INT equivalent - matched bool // Non use route matched + app *App // Reference to *App + route *Route // Reference to *Route + fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx + bind *Bind // Default bind reference + redirect *Redirect // Default redirect reference + req *DefaultReq // Default request api reference + res *DefaultRes // Default response api reference + values [maxParams]string // Route parameter values + viewBindMap sync.Map // Default view map to bind template engine + method string // HTTP method + baseURI string // HTTP base uri + treePath string // Path for the search in the tree + pathOriginal string // Original HTTP path + flashMessages redirectionMsgs // Flash messages + path []byte // HTTP path with the modifications by the configuration + detectionPath []byte // Route detection path + indexRoute int // Index of the current route + indexHandler int // Index of the current handler + methodINT int // HTTP method INT equivalent + matched bool // Non use route matched } // SendFile defines configuration options when to transfer file with SendFile. @@ -1123,8 +1121,9 @@ func Params[V GenericType](c Ctx, key string, defaultValue ...V) V { // Path returns the path part of the request URL. // Optionally, you could override the path. +// Make copies or use the Immutable setting to use the value outside the Handler. func (c *DefaultCtx) Path(override ...string) string { - if len(override) != 0 && c.path != override[0] { + if len(override) != 0 && string(c.path) != override[0] { // Set new path to context c.pathOriginal = override[0] @@ -1133,7 +1132,7 @@ func (c *DefaultCtx) Path(override ...string) string { // Prettify path c.configDependentPaths() } - return c.path + return c.app.getString(c.path) } // Scheme contains the request protocol string: http or https for TLS requests. @@ -1832,32 +1831,32 @@ func (c *DefaultCtx) XHR() bool { // configDependentPaths set paths for route recognition and prepared paths for the user, // here the features for caseSensitive, decoded paths, strict paths are evaluated func (c *DefaultCtx) configDependentPaths() { - c.pathBuffer = append(c.pathBuffer[0:0], c.pathOriginal...) + c.path = append(c.path[:0], c.pathOriginal...) // If UnescapePath enabled, we decode the path and save it for the framework user if c.app.config.UnescapePath { - c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer) + c.path = fasthttp.AppendUnquotedArg(c.path[:0], c.path) } - c.path = c.app.getString(c.pathBuffer) // another path is specified which is for routing recognition only // use the path that was changed by the previous configuration flags - c.detectionPathBuffer = append(c.detectionPathBuffer[0:0], c.pathBuffer...) + c.detectionPath = append(c.detectionPath[:0], c.path...) // If CaseSensitive is disabled, we lowercase the original path if !c.app.config.CaseSensitive { - c.detectionPathBuffer = utils.ToLowerBytes(c.detectionPathBuffer) + c.detectionPath = utils.ToLowerBytes(c.detectionPath) } // If StrictRouting is disabled, we strip all trailing slashes - if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' { - c.detectionPathBuffer = utils.TrimRight(c.detectionPathBuffer, '/') + if !c.app.config.StrictRouting && len(c.detectionPath) > 1 && c.detectionPath[len(c.detectionPath)-1] == '/' { + c.detectionPath = utils.TrimRight(c.detectionPath, '/') } - c.detectionPath = c.app.getString(c.detectionPathBuffer) // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, // since the first three characters area select a list of routes - c.treePath = c.treePath[0:0] + c.treePath = "" const maxDetectionPaths = 3 if len(c.detectionPath) >= maxDetectionPaths { - c.treePath = c.detectionPath[:maxDetectionPaths] + // c.treePath is only used by Fiber and is not exposed to the user + // so we can use utils.UnsafeString instead of c.app.getString + c.treePath = utils.UnsafeString(c.detectionPath[:maxDetectionPaths]) } } @@ -1963,7 +1962,7 @@ func (c *DefaultCtx) getTreePath() string { } func (c *DefaultCtx) getDetectionPath() string { - return c.detectionPath + return c.app.getString(c.detectionPath) } func (c *DefaultCtx) getPathOriginal() string { diff --git a/router.go b/router.go index 795529bd..be289ca5 100644 --- a/router.go +++ b/router.go @@ -180,7 +180,7 @@ func (app *App) next(c *DefaultCtx) (bool, error) { } // Check if it matches the request path - match = route.match(c.detectionPath, c.path, &c.values) + match = route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) if !match { // No match, next route continue From 7606c618d3e4950e9be66c3cbaff8aa457aa78f3 Mon Sep 17 00:00:00 2001 From: Mazyar Yousefiniyae shad Date: Tue, 25 Mar 2025 10:55:56 +0330 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20Add=20more=20validati?= =?UTF-8?q?on=20examples=20(#3369)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add examples on valudator guid * ref: return prev validation comment --- docs/guide/validation.md | 71 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 7226347f..fd007a62 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -8,8 +8,7 @@ sidebar_position: 5 Fiber provides the [Bind](../api/bind.md#validation) function to validate and bind [request data](../api/bind.md#binders) to a struct. -```go title="Example" - +```go title="Basic Example" import "github.com/go-playground/validator/v10" type structValidator struct { @@ -42,3 +41,71 @@ app.Post("/", func(c fiber.Ctx) error { return c.JSON(user) }) ``` + +```go title="Advanced Validation Example" +type User struct { + Name string `json:"name" validate:"required,min=3,max=32"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"gte=0,lte=100"` + Password string `json:"password" validate:"required,min=8"` + Website string `json:"website" validate:"url"` +} + +// Custom validation error messages +type UserWithCustomMessages struct { + Name string `json:"name" validate:"required,min=3,max=32" message:"Name is required and must be between 3 and 32 characters"` + Email string `json:"email" validate:"required,email" message:"Valid email is required"` + Age int `json:"age" validate:"gte=0,lte=100" message:"Age must be between 0 and 100"` +} + +app.Post("/user", func(c fiber.Ctx) error { + user := new(User) + + if err := c.Bind().Body(user); err != nil { + // Handle validation errors + if validationErrors, ok := err.(validator.ValidationErrors); ok { + for _, e := range validationErrors { + // e.Field() - field name + // e.Tag() - validation tag + // e.Value() - invalid value + // e.Param() - validation parameter + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "field": e.Field(), + "error": e.Error(), + }) + } + } + return err + } + + return c.JSON(user) +}) +``` + +```go title="Custom Validator Example" +// Custom validator for password strength +type PasswordValidator struct { + validate *validator.Validate +} + +func (v *PasswordValidator) Validate(out any) error { + if err := v.validate.Struct(out); err != nil { + return err + } + + // Custom password validation logic + if user, ok := out.(*User); ok { + if len(user.Password) < 8 { + return errors.New("password must be at least 8 characters") + } + // Add more password validation rules here + } + + return nil +} + +// Usage +app := fiber.New(fiber.Config{ + StructValidator: &PasswordValidator{validate: validator.New()}, +}) +``` From 4bf292945df965cb5fcf0e36cbe7fd2abc288084 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:30:41 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E2=99=BB=EF=B8=8FRefactor:=20reduce=20Defa?= =?UTF-8?q?ultCtx=20from=20736=20to=20728=20bytes=20(#3368)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 4 ++-- ctx.go | 22 ++++++++++++---------- ctx_interface.go | 2 +- ctx_interface_gen.go | 2 +- helpers.go | 8 ++++---- router.go | 30 ++++++++++++++---------------- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app.go b/app.go index 84059e79..d4d89d92 100644 --- a/app.go +++ b/app.go @@ -109,7 +109,7 @@ type App struct { // Route stack divided by HTTP methods stack [][]*Route // Route stack divided by HTTP methods and route prefixes - treeStack []map[string][]*Route + treeStack []map[int][]*Route // custom binders customBinders []CustomBinder // customConstraints is a list of external constraints @@ -581,7 +581,7 @@ func New(config ...Config) *App { // Create router stack app.stack = make([][]*Route, len(app.config.RequestMethods)) - app.treeStack = make([]map[string][]*Route, len(app.config.RequestMethods)) + app.treeStack = make([]map[int][]*Route, len(app.config.RequestMethods)) // Override colors app.config.ColorScheme = defaultColors(app.config.ColorScheme) diff --git a/ctx.go b/ctx.go index efd6006d..b959a48f 100644 --- a/ctx.go +++ b/ctx.go @@ -33,8 +33,11 @@ const ( schemeHTTPS = "https" ) -// maxParams defines the maximum number of parameters per route. -const maxParams = 30 +const ( + // maxParams defines the maximum number of parameters per route. + maxParams = 30 + maxDetectionPaths = 3 +) // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. @@ -60,11 +63,11 @@ type DefaultCtx struct { viewBindMap sync.Map // Default view map to bind template engine method string // HTTP method baseURI string // HTTP base uri - treePath string // Path for the search in the tree pathOriginal string // Original HTTP path flashMessages redirectionMsgs // Flash messages path []byte // HTTP path with the modifications by the configuration detectionPath []byte // Route detection path + treePathHash int // Hash of the path for the search in the tree indexRoute int // Index of the current route indexHandler int // Index of the current handler methodINT int // HTTP method INT equivalent @@ -1851,12 +1854,11 @@ func (c *DefaultCtx) configDependentPaths() { // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, // since the first three characters area select a list of routes - c.treePath = "" - const maxDetectionPaths = 3 + c.treePathHash = 0 if len(c.detectionPath) >= maxDetectionPaths { - // c.treePath is only used by Fiber and is not exposed to the user - // so we can use utils.UnsafeString instead of c.app.getString - c.treePath = utils.UnsafeString(c.detectionPath[:maxDetectionPaths]) + c.treePathHash = int(c.detectionPath[0])<<16 | + int(c.detectionPath[1])<<8 | + int(c.detectionPath[2]) } } @@ -1957,8 +1959,8 @@ func (c *DefaultCtx) getIndexRoute() int { return c.indexRoute } -func (c *DefaultCtx) getTreePath() string { - return c.treePath +func (c *DefaultCtx) getTreePathHash() int { + return c.treePathHash } func (c *DefaultCtx) getDetectionPath() string { diff --git a/ctx_interface.go b/ctx_interface.go index 32e8ee39..6ef33847 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -19,7 +19,7 @@ type CustomCtx interface { // Methods to use with next stack. getMethodINT() int getIndexRoute() int - getTreePath() string + getTreePathHash() int getDetectionPath() string getPathOriginal() string getValues() *[maxParams]string diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index fffe218d..a4d7db3d 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -347,7 +347,7 @@ type Ctx interface { // Methods to use with next stack. getMethodINT() int getIndexRoute() int - getTreePath() string + getTreePathHash() int getDetectionPath() string getPathOriginal() string getValues() *[maxParams]string diff --git a/helpers.go b/helpers.go index 584553a7..3f1685b1 100644 --- a/helpers.go +++ b/helpers.go @@ -113,9 +113,9 @@ func (app *App) methodExist(c *DefaultCtx) bool { // Reset stack index c.setIndexRoute(-1) - tree, ok := c.App().treeStack[i][c.getTreePath()] + tree, ok := c.App().treeStack[i][c.treePathHash] if !ok { - tree = c.App().treeStack[i][""] + tree = c.App().treeStack[i][0] } // Get stack length lenr := len(tree) - 1 @@ -157,9 +157,9 @@ func (app *App) methodExistCustom(c CustomCtx) bool { // Reset stack index c.setIndexRoute(-1) - tree, ok := c.App().treeStack[i][c.getTreePath()] + tree, ok := c.App().treeStack[i][c.getTreePathHash()] if !ok { - tree = c.App().treeStack[i][""] + tree = c.App().treeStack[i][0] } // Get stack length lenr := len(tree) - 1 diff --git a/router.go b/router.go index be289ca5..0aec6509 100644 --- a/router.go +++ b/router.go @@ -110,9 +110,9 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing // Get stack length - tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()] + tree, ok := app.treeStack[c.getMethodINT()][c.getTreePathHash()] if !ok { - tree = app.treeStack[c.getMethodINT()][""] + tree = app.treeStack[c.getMethodINT()][0] } lenr := len(tree) - 1 @@ -158,9 +158,9 @@ func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool func (app *App) next(c *DefaultCtx) (bool, error) { // Get stack length - tree, ok := app.treeStack[c.methodINT][c.treePath] + tree, ok := app.treeStack[c.methodINT][c.treePathHash] if !ok { - tree = app.treeStack[c.methodINT][""] + tree = app.treeStack[c.methodINT][0] } lenTree := len(tree) - 1 @@ -454,30 +454,28 @@ func (app *App) buildTree() *App { // loop all the methods and stacks and create the prefix tree for m := range app.config.RequestMethods { - tsMap := make(map[string][]*Route) + tsMap := make(map[int][]*Route) for _, route := range app.stack[m] { - treePath := "" - if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { - treePath = route.routeParser.segs[0].Const[:3] + treePathHash := 0 + if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= maxDetectionPaths { + treePathHash = int(route.routeParser.segs[0].Const[0])<<16 | + int(route.routeParser.segs[0].Const[1])<<8 | + int(route.routeParser.segs[0].Const[2]) } // create tree stack - tsMap[treePath] = append(tsMap[treePath], route) + tsMap[treePathHash] = append(tsMap[treePathHash], route) } - app.treeStack[m] = tsMap - } - // loop the methods and tree stacks and add global stack and sort everything - for m := range app.config.RequestMethods { - tsMap := app.treeStack[m] for treePart := range tsMap { - if treePart != "" { + if treePart != 0 { // merge global tree routes in current tree stack - tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...)) + tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[0]...)) } // sort tree slices with the positions slc := tsMap[treePart] sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) } + app.treeStack[m] = tsMap } app.routesRefreshed = false