Merge branch 'main' into revert-3287-context-middleware

revert-3287-context-middleware
Juan Calderon-Perez 2025-03-25 07:03:41 -04:00 committed by GitHub
commit 56c7372c50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 403 additions and 140 deletions

4
app.go
View File

@ -109,7 +109,7 @@ type App struct {
// Route stack divided by HTTP methods // Route stack divided by HTTP methods
stack [][]*Route stack [][]*Route
// Route stack divided by HTTP methods and route prefixes // Route stack divided by HTTP methods and route prefixes
treeStack []map[string][]*Route treeStack []map[int][]*Route
// custom binders // custom binders
customBinders []CustomBinder customBinders []CustomBinder
// customConstraints is a list of external constraints // customConstraints is a list of external constraints
@ -581,7 +581,7 @@ func New(config ...Config) *App {
// Create router stack // Create router stack
app.stack = make([][]*Route, len(app.config.RequestMethods)) 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 // Override colors
app.config.ColorScheme = defaultColors(app.config.ColorScheme) app.config.ColorScheme = defaultColors(app.config.ColorScheme)

81
ctx.go
View File

@ -33,8 +33,11 @@ const (
schemeHTTPS = "https" schemeHTTPS = "https"
) )
// maxParams defines the maximum number of parameters per route. const (
const maxParams = 30 // 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 // The contextKey type is unexported to prevent collisions with context keys defined in
// other packages. // other packages.
@ -49,28 +52,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." //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 { type DefaultCtx struct {
app *App // Reference to *App app *App // Reference to *App
route *Route // Reference to *Route route *Route // Reference to *Route
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
bind *Bind // Default bind reference bind *Bind // Default bind reference
redirect *Redirect // Default redirect reference redirect *Redirect // Default redirect reference
req *DefaultReq // Default request api reference req *DefaultReq // Default request api reference
res *DefaultRes // Default response api reference res *DefaultRes // Default response api reference
values [maxParams]string // Route parameter values values [maxParams]string // Route parameter values
viewBindMap sync.Map // Default view map to bind template engine viewBindMap sync.Map // Default view map to bind template engine
method string // HTTP method method string // HTTP method
baseURI string // HTTP base uri baseURI string // HTTP base uri
path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer pathOriginal string // Original HTTP path
detectionPath string // Route detection path -> string copy from detectionPathBuffer flashMessages redirectionMsgs // Flash messages
treePath string // Path for the search in the tree path []byte // HTTP path with the modifications by the configuration
pathOriginal string // Original HTTP path detectionPath []byte // Route detection path
pathBuffer []byte // HTTP path buffer treePathHash int // Hash of the path for the search in the tree
detectionPathBuffer []byte // HTTP detectionPath buffer indexRoute int // Index of the current route
flashMessages redirectionMsgs // Flash messages indexHandler int // Index of the current handler
indexRoute int // Index of the current route methodINT int // HTTP method INT equivalent
indexHandler int // Index of the current handler matched bool // Non use route matched
methodINT int // HTTP method INT equivalent
matched bool // Non use route matched
} }
// SendFile defines configuration options when to transfer file with SendFile. // SendFile defines configuration options when to transfer file with SendFile.
@ -1123,8 +1124,9 @@ func Params[V GenericType](c Ctx, key string, defaultValue ...V) V {
// Path returns the path part of the request URL. // Path returns the path part of the request URL.
// Optionally, you could override the path. // 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 { 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 // Set new path to context
c.pathOriginal = override[0] c.pathOriginal = override[0]
@ -1133,7 +1135,7 @@ func (c *DefaultCtx) Path(override ...string) string {
// Prettify path // Prettify path
c.configDependentPaths() c.configDependentPaths()
} }
return c.path return c.app.getString(c.path)
} }
// Scheme contains the request protocol string: http or https for TLS requests. // Scheme contains the request protocol string: http or https for TLS requests.
@ -1832,32 +1834,31 @@ func (c *DefaultCtx) XHR() bool {
// configDependentPaths set paths for route recognition and prepared paths for the user, // configDependentPaths set paths for route recognition and prepared paths for the user,
// here the features for caseSensitive, decoded paths, strict paths are evaluated // here the features for caseSensitive, decoded paths, strict paths are evaluated
func (c *DefaultCtx) configDependentPaths() { 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 UnescapePath enabled, we decode the path and save it for the framework user
if c.app.config.UnescapePath { 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 // another path is specified which is for routing recognition only
// use the path that was changed by the previous configuration flags // 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 CaseSensitive is disabled, we lowercase the original path
if !c.app.config.CaseSensitive { 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 StrictRouting is disabled, we strip all trailing slashes
if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' { if !c.app.config.StrictRouting && len(c.detectionPath) > 1 && c.detectionPath[len(c.detectionPath)-1] == '/' {
c.detectionPathBuffer = utils.TrimRight(c.detectionPathBuffer, '/') 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, // 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 // since the first three characters area select a list of routes
c.treePath = c.treePath[0:0] c.treePathHash = 0
const maxDetectionPaths = 3
if len(c.detectionPath) >= maxDetectionPaths { if len(c.detectionPath) >= maxDetectionPaths {
c.treePath = c.detectionPath[:maxDetectionPaths] c.treePathHash = int(c.detectionPath[0])<<16 |
int(c.detectionPath[1])<<8 |
int(c.detectionPath[2])
} }
} }
@ -1958,12 +1959,12 @@ func (c *DefaultCtx) getIndexRoute() int {
return c.indexRoute return c.indexRoute
} }
func (c *DefaultCtx) getTreePath() string { func (c *DefaultCtx) getTreePathHash() int {
return c.treePath return c.treePathHash
} }
func (c *DefaultCtx) getDetectionPath() string { func (c *DefaultCtx) getDetectionPath() string {
return c.detectionPath return c.app.getString(c.detectionPath)
} }
func (c *DefaultCtx) getPathOriginal() string { func (c *DefaultCtx) getPathOriginal() string {

View File

@ -19,7 +19,7 @@ type CustomCtx interface {
// Methods to use with next stack. // Methods to use with next stack.
getMethodINT() int getMethodINT() int
getIndexRoute() int getIndexRoute() int
getTreePath() string getTreePathHash() int
getDetectionPath() string getDetectionPath() string
getPathOriginal() string getPathOriginal() string
getValues() *[maxParams]string getValues() *[maxParams]string

View File

@ -347,7 +347,7 @@ type Ctx interface {
// Methods to use with next stack. // Methods to use with next stack.
getMethodINT() int getMethodINT() int
getIndexRoute() int getIndexRoute() int
getTreePath() string getTreePathHash() int
getDetectionPath() string getDetectionPath() string
getPathOriginal() string getPathOriginal() string
getValues() *[maxParams]string getValues() *[maxParams]string

View File

@ -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. 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" import "github.com/go-playground/validator/v10"
type structValidator struct { type structValidator struct {
@ -42,3 +41,71 @@ app.Post("/", func(c fiber.Ctx) error {
return c.JSON(user) 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()},
})
```

View File

@ -79,7 +79,7 @@ app.Use(logger.New(logger.Config{
TimeZone: "Asia/Shanghai", TimeZone: "Asia/Shanghai",
Done: func(c fiber.Ctx, logString []byte) { Done: func(c fiber.Ctx, logString []byte) {
if c.Response().StatusCode() != fiber.StatusOK { 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{ app.Use(logger.New(logger.Config{
DisableColors: true, 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 ### 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 ### Config
| Property | Type | Description | Default | | Property | Type | Description | Default |
|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------| | :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | 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` | | 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` | | 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` | | 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` | | `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` | | 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"` | | 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` | | 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` | | 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` | | 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` | | DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` |
## Default Config ## Default Config
```go ```go
var ConfigDefault = Config{ var ConfigDefault = Config{
Next: nil, Next: nil,
Skip nil, Skip: nil,
Done: nil, Done: nil,
Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n", Format: DefaultFormat,
TimeFormat: "15:04:05", TimeFormat: "15:04:05",
TimeZone: "Local", TimeZone: "Local",
TimeInterval: 500 * time.Millisecond, TimeInterval: 500 * time.Millisecond,
Stream: os.Stdout, Stream: os.Stdout,
DisableColors: false, BeforeHandlerFunc: beforeHandlerFunc,
LoggerFunc: defaultLoggerInstance, 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 ## Constants
```go ```go

View File

@ -937,6 +937,22 @@ app.Use(logger.New(logger.Config{
</details> </details>
#### Predefined Formats
Logger provides predefined formats that you can use by name or directly by specifying the format string.
<details>
<summary>Example Usage</summary>
```go
app.Use(logger.New(logger.Config{
Format: logger.FormatCombined,
}))
```
See more in [Logger](./middleware/logger.md#predefined-formats)
</details>
### Filesystem ### Filesystem
We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware.

View File

@ -113,9 +113,9 @@ func (app *App) methodExist(c *DefaultCtx) bool {
// Reset stack index // Reset stack index
c.setIndexRoute(-1) c.setIndexRoute(-1)
tree, ok := c.App().treeStack[i][c.getTreePath()] tree, ok := c.App().treeStack[i][c.treePathHash]
if !ok { if !ok {
tree = c.App().treeStack[i][""] tree = c.App().treeStack[i][0]
} }
// Get stack length // Get stack length
lenr := len(tree) - 1 lenr := len(tree) - 1
@ -157,9 +157,9 @@ func (app *App) methodExistCustom(c CustomCtx) bool {
// Reset stack index // Reset stack index
c.setIndexRoute(-1) c.setIndexRoute(-1)
tree, ok := c.App().treeStack[i][c.getTreePath()] tree, ok := c.App().treeStack[i][c.getTreePathHash()]
if !ok { if !ok {
tree = c.App().treeStack[i][""] tree = c.App().treeStack[i][0]
} }
// Get stack length // Get stack length
lenr := len(tree) - 1 lenr := len(tree) - 1

View File

@ -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 // 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) { func Test_CSRF_UnsafeHeaderValue(t *testing.T) {
// t.Parallel() t.SkipNow()
// app := fiber.New() t.Parallel()
app := fiber.New()
// app.Use(New()) app.Use(New())
// app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
// return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
// }) })
// app.Get("/test", func(c fiber.Ctx) error { app.Get("/test", func(c fiber.Ctx) error {
// return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
// }) })
// app.Post("/", func(c fiber.Ctx) error { app.Post("/", func(c fiber.Ctx) error {
// return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
// }) })
// resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
// require.NoError(t, err) require.NoError(t, err)
// require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, fiber.StatusOK, resp.StatusCode)
// var token string var token string
// for _, c := range resp.Cookies() { for _, c := range resp.Cookies() {
// if c.Name != ConfigDefault.CookieName { if c.Name != ConfigDefault.CookieName {
// continue continue
// } }
// token = c.Value token = c.Value
// break break
// } }
// fmt.Println("token", token) t.Log("token", token)
// getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) getReq := httptest.NewRequest(fiber.MethodGet, "/", nil)
// getReq.Header.Set(HeaderName, token) 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 = httptest.NewRequest(fiber.MethodGet, "/test", nil) getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil)
// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") getReq.Header.Set("X-Requested-With", "XMLHttpRequest")
// getReq.Header.Set(fiber.HeaderCacheControl, "no") getReq.Header.Set(fiber.HeaderCacheControl, "no")
// getReq.Header.Set(HeaderName, token) 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.Set(fiber.HeaderAccept, "*/*")
// getReq.Header.Del(HeaderName) getReq.Header.Del(HeaderName)
// resp, err = app.Test(getReq) resp, err = app.Test(getReq)
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
// postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) postReq := httptest.NewRequest(fiber.MethodPost, "/", nil)
// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") postReq.Header.Set("X-Requested-With", "XMLHttpRequest")
// postReq.Header.Set(HeaderName, token) postReq.Header.Set(HeaderName, token)
// resp, err = app.Test(postReq) 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 // go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4
func Benchmark_Middleware_CSRF_Check(b *testing.B) { func Benchmark_Middleware_CSRF_Check(b *testing.B) {

View File

@ -50,9 +50,23 @@ type Config struct {
timeZoneLocation *time.Location 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 Format string
// TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html
@ -105,7 +119,7 @@ var ConfigDefault = Config{
Next: nil, Next: nil,
Skip: nil, Skip: nil,
Done: nil, Done: nil,
Format: defaultFormat, Format: DefaultFormat,
TimeFormat: "15:04:05", TimeFormat: "15:04:05",
TimeZone: "Local", TimeZone: "Local",
TimeInterval: 500 * time.Millisecond, TimeInterval: 500 * time.Millisecond,
@ -115,9 +129,6 @@ var ConfigDefault = Config{
enableColors: true, 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 // Helper function to set default values
func configDefault(config ...Config) Config { func configDefault(config ...Config) Config {
// Return default config if nothing provided // Return default config if nothing provided

View File

@ -28,7 +28,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
buf := bytebufferpool.Get() buf := bytebufferpool.Get()
// Default output when no custom Format or io.Writer is given // Default output when no custom Format or io.Writer is given
if cfg.Format == defaultFormat { if cfg.Format == DefaultFormat {
// Format error if exist // Format error if exist
formatErr := "" formatErr := ""
if cfg.enableColors { if cfg.enableColors {

View File

@ -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"
)

View File

@ -40,7 +40,6 @@ func New(config ...Config) fiber.Handler {
} }
}() }()
} }
// Set PID once // Set PID once
pid := strconv.Itoa(os.Getpid()) pid := strconv.Itoa(os.Getpid())

View File

@ -467,6 +467,124 @@ func Test_Logger_All(t *testing.T) {
require.Equal(t, expected, buf.String()) 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 { func getLatencyTimeUnits() []struct {
unit string unit string
div time.Duration div time.Duration

View File

@ -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 func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing
// Get stack length // Get stack length
tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()] tree, ok := app.treeStack[c.getMethodINT()][c.getTreePathHash()]
if !ok { if !ok {
tree = app.treeStack[c.getMethodINT()][""] tree = app.treeStack[c.getMethodINT()][0]
} }
lenr := len(tree) - 1 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) { func (app *App) next(c *DefaultCtx) (bool, error) {
// Get stack length // Get stack length
tree, ok := app.treeStack[c.methodINT][c.treePath] tree, ok := app.treeStack[c.methodINT][c.treePathHash]
if !ok { if !ok {
tree = app.treeStack[c.methodINT][""] tree = app.treeStack[c.methodINT][0]
} }
lenTree := len(tree) - 1 lenTree := len(tree) - 1
@ -180,7 +180,7 @@ func (app *App) next(c *DefaultCtx) (bool, error) {
} }
// Check if it matches the request path // 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 { if !match {
// No match, next route // No match, next route
continue continue
@ -454,30 +454,28 @@ func (app *App) buildTree() *App {
// loop all the methods and stacks and create the prefix tree // loop all the methods and stacks and create the prefix tree
for m := range app.config.RequestMethods { for m := range app.config.RequestMethods {
tsMap := make(map[string][]*Route) tsMap := make(map[int][]*Route)
for _, route := range app.stack[m] { for _, route := range app.stack[m] {
treePath := "" treePathHash := 0
if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= maxDetectionPaths {
treePath = route.routeParser.segs[0].Const[:3] 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 // 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 { for treePart := range tsMap {
if treePart != "" { if treePart != 0 {
// merge global tree routes in current tree stack // 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 // sort tree slices with the positions
slc := tsMap[treePart] slc := tsMap[treePart]
sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos })
} }
app.treeStack[m] = tsMap
} }
app.routesRefreshed = false app.routesRefreshed = false