Merge remote-tracking branch 'refs/remotes/origin/main' into generic_app_ctx_3221

# Conflicts:
#	app.go
#	ctx.go
#	ctx_interface.go
#	ctx_interface_gen.go
#	helpers.go
#	listen.go
#	router.go
generic_app_ctx_3221
René 2025-03-26 14:30:38 +01:00
commit 334612f5b2
59 changed files with 2816 additions and 1544 deletions

View File

@ -17,6 +17,7 @@ categories:
- title: '🧹 Updates' - title: '🧹 Updates'
labels: labels:
- '🧹 Updates' - '🧹 Updates'
- '⚡️ Performance'
- title: '🐛 Fixes' - title: '🐛 Fixes'
labels: labels:
- '☢️ Bug' - '☢️ Bug'
@ -48,6 +49,7 @@ version-resolver:
- '☢️ Bug' - '☢️ Bug'
- '🤖 Dependencies' - '🤖 Dependencies'
- '🧹 Updates' - '🧹 Updates'
- '⚡️ Performance'
default: patch default: patch
template: | template: |
$CHANGES $CHANGES

1
.github/release.yml vendored
View File

@ -12,6 +12,7 @@ changelog:
- title: '🧹 Updates' - title: '🧹 Updates'
labels: labels:
- '🧹 Updates' - '🧹 Updates'
- '⚡️ Performance'
- title: '🐛 Bug Fixes' - title: '🐛 Bug Fixes'
labels: labels:
- '☢️ Bug' - '☢️ Bug'

View File

@ -3,11 +3,11 @@ on:
branches: branches:
- master - master
- main - main
paths-ignore: paths:
- "**/*.md" - "**.go"
pull_request: pull_request:
paths-ignore: paths:
- "**/*.md" - "**.go"
permissions: permissions:
# deployments permission to deploy GitHub pages website # deployments permission to deploy GitHub pages website

View File

@ -37,4 +37,7 @@ jobs:
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:
# NOTE: Keep this in sync with the version from .golangci.yml # NOTE: Keep this in sync with the version from .golangci.yml
version: v1.62.2 version: v1.64.7
# NOTE(ldez): temporary workaround
install-mode: goinstall

View File

@ -32,7 +32,7 @@ jobs:
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.23.x' }} if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.23.x' }}
uses: codecov/codecov-action@v5.3.1 uses: codecov/codecov-action@v5.4.0
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.txt file: ./coverage.txt

View File

@ -7,7 +7,6 @@ run:
output: output:
sort-results: true sort-results: true
uniq-by-line: false
linters-settings: linters-settings:
depguard: depguard:
@ -187,7 +186,7 @@ linters-settings:
- name: unchecked-type-assertion - name: unchecked-type-assertion
disabled: true # TODO: Do not disable disabled: true # TODO: Do not disable
- name: unhandled-error - name: unhandled-error
arguments: ['bytes\.Buffer\.Write'] disabled: true
stylecheck: stylecheck:
checks: checks:
@ -250,7 +249,10 @@ issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclude-dirs: exclude-dirs:
- internal # TODO: Do not ignore interal packages - internal # TODO: Do not ignore internal packages
exclude-files:
- '_msgp\.go'
- '_msgp_test\.go'
exclude-rules: exclude-rules:
- linters: - linters:
- err113 - err113
@ -263,7 +265,10 @@ issues:
linters: linters:
- bodyclose - bodyclose
- err113 - err113
# fix: true - source: 'fmt.Fprintf?'
linters:
- errcheck
- revive
linters: linters:
enable: enable:
@ -358,7 +363,6 @@ linters:
- stylecheck - stylecheck
# - tagalign # TODO: Enable # - tagalign # TODO: Enable
- tagliatelle - tagliatelle
- tenv
- testableexamples - testableexamples
- testifylint - testifylint
# - testpackage # TODO: Enable # - testpackage # TODO: Enable

View File

@ -35,7 +35,7 @@ markdown:
## lint: 🚨 Run lint checks ## lint: 🚨 Run lint checks
.PHONY: lint .PHONY: lint
lint: lint:
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run ./... golangci-lint run
## test: 🚦 Execute all tests ## test: 🚦 Execute all tests
.PHONY: test .PHONY: test

View File

@ -19,17 +19,47 @@ a jitter is a way to break synchronization across the client and avoid collision
## Signatures ## Signatures
```go ```go
func NewExponentialBackoff(config ...Config) *ExponentialBackoff func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff
``` ```
## Examples ## Examples
Firstly, import the addon from Fiber,
```go ```go
package main
import ( import (
"fmt"
"github.com/gofiber/fiber/v3/addon/retry" "github.com/gofiber/fiber/v3/addon/retry"
"github.com/gofiber/fiber/v3/client"
) )
func main() {
expBackoff := retry.NewExponentialBackoff(retry.Config{})
// Local variables that will be used inside of Retry
var resp *client.Response
var err error
// Retry a network request and return an error to signify to try again
err = expBackoff.Retry(func() error {
client := client.New()
resp, err = client.Get("https://gofiber.io")
if err != nil {
return fmt.Errorf("GET gofiber.io failed: %w", err)
}
if resp.StatusCode() != 200 {
return fmt.Errorf("GET gofiber.io did not return OK 200")
}
return nil
})
// If all retries failed, panic
if err != nil {
panic(err)
}
fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode())
}
``` ```
## Default Config ## Default Config
@ -75,11 +105,6 @@ type Config struct {
// //
// Optional. Default: 10 // Optional. Default: 10
MaxRetryCount int MaxRetryCount int
// currentInterval tracks the current waiting time.
//
// Optional. Default: 1 * time.Second
currentInterval time.Duration
} }
``` ```

36
app.go
View File

@ -109,7 +109,7 @@ type App[TCtx CtxGeneric[TCtx]] struct {
// Route stack divided by HTTP methods // Route stack divided by HTTP methods
stack [][]*Route[TCtx] stack [][]*Route[TCtx]
// Route stack divided by HTTP methods and route prefixes // Route stack divided by HTTP methods and route prefixes
treeStack []map[string][]*Route[TCtx] treeStack []map[int][]*Route[TCtx]
// custom binders // custom binders
customBinders []CustomBinder customBinders []CustomBinder
// customConstraints is a list of external constraints // customConstraints is a list of external constraints
@ -456,17 +456,29 @@ const (
DefaultWriteBufferSize = 4096 DefaultWriteBufferSize = 4096
) )
const (
methodGet = iota
methodHead
methodPost
methodPut
methodDelete
methodConnect
methodOptions
methodTrace
methodPatch
)
// HTTP methods enabled by default // HTTP methods enabled by default
var DefaultMethods = []string{ var DefaultMethods = []string{
MethodGet, methodGet: MethodGet,
MethodHead, methodHead: MethodHead,
MethodPost, methodPost: MethodPost,
MethodPut, methodPut: MethodPut,
MethodDelete, methodDelete: MethodDelete,
MethodConnect, methodConnect: MethodConnect,
MethodOptions, methodOptions: MethodOptions,
MethodTrace, methodTrace: MethodTrace,
MethodPatch, methodPatch: MethodPatch,
} }
// DefaultErrorHandler that process return errors from handlers // DefaultErrorHandler that process return errors from handlers
@ -621,7 +633,7 @@ func newApp[TCtx CtxGeneric[TCtx]](config ...Config[TCtx]) *App[TCtx] {
// Create router stack // Create router stack
app.stack = make([][]*Route[TCtx], len(app.config.RequestMethods)) app.stack = make([][]*Route[TCtx], len(app.config.RequestMethods))
app.treeStack = make([]map[string][]*Route[TCtx], len(app.config.RequestMethods)) app.treeStack = make([]map[int][]*Route[TCtx], len(app.config.RequestMethods))
// Override colors // Override colors
app.config.ColorScheme = defaultColors(app.config.ColorScheme) app.config.ColorScheme = defaultColors(app.config.ColorScheme)
@ -1048,7 +1060,7 @@ func (app *App[TCtx]) Test(req *http.Request, config ...TestConfig) (*http.Respo
select { select {
case err = <-channel: case err = <-channel:
case <-time.After(cfg.Timeout): case <-time.After(cfg.Timeout):
conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here conn.Close() //nolint:errcheck // It is fine to ignore the error here
if cfg.FailOnTimeout { if cfg.FailOnTimeout {
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }

View File

@ -403,7 +403,7 @@ func Test_App_serverErrorHandler_Internal_Error(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
msg := "test err" msg := "test err"
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
app.serverErrorHandler(c.fasthttp, errors.New(msg)) app.serverErrorHandler(c.fasthttp, errors.New(msg))
require.Equal(t, string(c.fasthttp.Response.Body()), msg) require.Equal(t, string(c.fasthttp.Response.Body()), msg)
@ -413,7 +413,7 @@ func Test_App_serverErrorHandler_Internal_Error(t *testing.T) {
func Test_App_serverErrorHandler_Network_Error(t *testing.T) { func Test_App_serverErrorHandler_Network_Error(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
app.serverErrorHandler(c.fasthttp, &net.DNSError{ app.serverErrorHandler(c.fasthttp, &net.DNSError{
Err: "test error", Err: "test error",

View File

@ -1378,7 +1378,7 @@ func Benchmark_Bind_URI(b *testing.B) {
var err error var err error
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.route = &Route{ c.route = &Route{
Params: []string{ Params: []string{
@ -1415,7 +1415,7 @@ func Benchmark_Bind_URI_Map(b *testing.B) {
var err error var err error
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.route = &Route{ c.route = &Route{
Params: []string{ Params: []string{

View File

@ -28,7 +28,7 @@ Fiber provides several default binders out of the box:
### Binding into a Struct ### Binding into a Struct
Fiber supports binding request data directly into a struct using [gorilla/schema](https://github.com/gorilla/schema). Here's an example: Fiber supports binding request data directly into a struct using [gofiber/schema](https://github.com/gofiber/schema). Here's an example:
```go ```go
// Field names must start with an uppercase letter // Field names must start with an uppercase letter

View File

@ -58,19 +58,19 @@ func Benchmark_FormBinder_Bind(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
binder := &QueryBinding{ binder := &FormBinding{
EnableSplitting: true, EnableSplitting: true,
} }
type User struct { type User struct {
Name string `query:"name"` Name string `form:"name"`
Posts []string `query:"posts"` Posts []string `form:"posts"`
Age int `query:"age"` Age int `form:"age"`
} }
var user User var user User
req := fasthttp.AcquireRequest() req := fasthttp.AcquireRequest()
req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3") req.SetBodyString("name=john&age=42&posts=post1,post2,post3")
req.Header.SetContentType("application/x-www-form-urlencoded") req.Header.SetContentType("application/x-www-form-urlencoded")
b.ResetTimer() b.ResetTimer()

View File

@ -87,7 +87,7 @@ func parse(aliasTag string, out any, data map[string][]string, files ...map[stri
return parseToStruct(aliasTag, out, data, files...) return parseToStruct(aliasTag, out, data, files...)
} }
// Parse data into the struct with gorilla/schema // Parse data into the struct with gofiber/schema
func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error {
// Get decoder from pool // Get decoder from pool
schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed

View File

@ -131,7 +131,7 @@ func testRequestFail(t *testing.T, handler fiber.Handler, wrapAgent func(agent *
} }
} }
func testClient(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Client), excepted string, count ...int) { //nolint: unparam // maybe needed func testClient(t *testing.T, handler fiber.Handler, wrapAgent func(agent *Client), excepted string, count ...int) { //nolint:unparam // maybe needed
t.Helper() t.Helper()
app, ln, start := createHelperServer(t) app, ln, start := createHelperServer(t)

View File

@ -199,7 +199,7 @@ func parserRequestBody(c *Client, req *Request) error {
case filesBody: case filesBody:
return parserRequestBodyFile(req) return parserRequestBodyFile(req)
case rawBody: case rawBody:
if body, ok := req.body.([]byte); ok { if body, ok := req.body.([]byte); ok { //nolint:revive // ignore simplicity
req.RawRequest.SetBody(body) req.RawRequest.SetBody(body)
} else { } else {
return ErrBodyType return ErrBodyType

View File

@ -298,11 +298,12 @@ func (r *Request) Cookie(key string) string {
// Use maps.Collect() to gather them into a map if needed. // Use maps.Collect() to gather them into a map if needed.
func (r *Request) Cookies() iter.Seq2[string, string] { func (r *Request) Cookies() iter.Seq2[string, string] {
return func(yield func(string, string) bool) { return func(yield func(string, string) bool) {
r.cookies.VisitAll(func(key, val string) { for k, v := range *r.cookies {
if !yield(key, val) { res := yield(k, v)
if !res {
return return
} }
}) }
} }
} }
@ -343,11 +344,11 @@ func (r *Request) PathParam(key string) string {
// Use maps.Collect() to gather them into a map if needed. // Use maps.Collect() to gather them into a map if needed.
func (r *Request) PathParams() iter.Seq2[string, string] { func (r *Request) PathParams() iter.Seq2[string, string] {
return func(yield func(string, string) bool) { return func(yield func(string, string) bool) {
r.path.VisitAll(func(key, val string) { for k, v := range *r.path {
if !yield(key, val) { if !yield(k, v) {
return return
} }
}) }
} }
} }

View File

@ -1,3 +1,4 @@
//nolint:goconst // Much easier to just ignore memory leaks in tests
package client package client
import ( import (
@ -451,6 +452,14 @@ func Test_Request_Cookies(t *testing.T) {
require.Equal(t, "bar", cookies["foo"]) require.Equal(t, "bar", cookies["foo"])
require.Equal(t, "foo", cookies["bar"]) require.Equal(t, "foo", cookies["bar"])
require.NotPanics(t, func() {
for _, v := range req.Cookies() {
if v == "bar" {
break
}
}
})
require.Len(t, cookies, 2) require.Len(t, cookies, 2)
} }
@ -564,6 +573,14 @@ func Test_Request_PathParams(t *testing.T) {
require.Equal(t, "foo", pathParams["bar"]) require.Equal(t, "foo", pathParams["bar"])
require.Len(t, pathParams, 2) require.Len(t, pathParams, 2)
require.NotPanics(t, func() {
for _, v := range req.PathParams() {
if v == "bar" {
break
}
}
})
} }
func Benchmark_Request_PathParams(b *testing.B) { func Benchmark_Request_PathParams(b *testing.B) {
@ -1579,7 +1596,7 @@ func Test_SetValWithStruct(t *testing.T) {
require.True(t, func() bool { require.True(t, func() bool {
for _, v := range p.PeekMulti("TSlice") { for _, v := range p.PeekMulti("TSlice") {
if string(v) == "bar" { //nolint:goconst // test if string(v) == "bar" {
return true return true
} }
} }

89
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.
@ -55,20 +58,19 @@ type DefaultCtx struct {
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
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
baseURI string // HTTP base uri 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 pathOriginal string // Original HTTP path
pathBuffer []byte // HTTP path buffer
detectionPathBuffer []byte // HTTP detectionPath buffer
flashMessages redirectionMsgs // Flash messages 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 indexRoute int // Index of the current route
indexHandler int // Index of the current handler indexHandler int // Index of the current handler
methodINT int // HTTP method INT equivalent methodInt int // HTTP method INT equivalent
matched bool // Non use route matched matched bool // Non use route matched
} }
@ -1006,19 +1008,17 @@ func (c *DefaultCtx) Location(path string) {
func (c *DefaultCtx) Method(override ...string) string { func (c *DefaultCtx) Method(override ...string) string {
if len(override) == 0 { if len(override) == 0 {
// Nothing to override, just return current method from context // Nothing to override, just return current method from context
return c.method return c.app.method(c.methodInt)
} }
method := utils.ToUpper(override[0]) method := utils.ToUpper(override[0])
mINT := c.app.methodInt(method) methodInt := c.app.methodInt(method)
if mINT == -1 { if methodInt == -1 {
// Provided override does not valid HTTP method, no override, return current method // Provided override does not valid HTTP method, no override, return current method
return c.method return c.app.method(c.methodInt)
} }
c.methodInt = methodInt
c.method = method return method
c.methodINT = mINT
return c.method
} }
// MultipartForm parse form entries from binary. // MultipartForm parse form entries from binary.
@ -1126,8 +1126,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]
@ -1136,7 +1137,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.
@ -1352,7 +1353,7 @@ func (c *DefaultCtx) getLocationFromRoute(route Route, params Map) (string, erro
for key, val := range params { for key, val := range params {
isSame := key == segment.ParamName || (!c.app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName)) isSame := key == segment.ParamName || (!c.app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName))
isGreedy := segment.IsGreedy && len(key) == 1 && isInCharset(key[0], greedyParameters) isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) != -1
if isSame || isGreedy { if isSame || isGreedy {
_, err := buf.WriteString(utils.ToString(val)) _, err := buf.WriteString(utils.ToString(val))
if err != nil { if err != nil {
@ -1468,6 +1469,18 @@ func (c *DefaultCtx) renderExtensions(bind any) {
} }
} }
// Req returns a convenience type whose API is limited to operations
// on the incoming request.
func (c *DefaultCtx) Req() Req {
return c.req
}
// Res returns a convenience type whose API is limited to operations
// on the outgoing response.
func (c *DefaultCtx) Res() Res {
return c.res
}
// Route returns the matched Route struct. // Route returns the matched Route struct.
func (c *DefaultCtx) Route() *Route { func (c *DefaultCtx) Route() *Route {
if c.route == nil { if c.route == nil {
@ -1475,7 +1488,7 @@ func (c *DefaultCtx) Route() *Route {
return &Route{ return &Route{
path: c.pathOriginal, path: c.pathOriginal,
Path: c.pathOriginal, Path: c.pathOriginal,
Method: c.method, Method: c.Method(),
Handlers: make([]Handler, 0), Handlers: make([]Handler, 0),
Params: make([]string, 0), Params: make([]string, 0),
} }
@ -1823,32 +1836,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])
} }
} }
@ -1909,8 +1921,7 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {
// Set paths // Set paths
c.pathOriginal = c.app.getString(fctx.URI().PathOriginal()) c.pathOriginal = c.app.getString(fctx.URI().PathOriginal())
// Set method // Set method
c.method = c.app.getString(fctx.Request.Header.Method()) c.methodInt = c.app.methodInt(utils.UnsafeString(fctx.Request.Header.Method()))
c.methodINT = c.app.methodInt(c.method)
// Attach *fasthttp.RequestCtx to ctx // Attach *fasthttp.RequestCtx to ctx
c.fasthttp = fctx c.fasthttp = fctx
// reset base uri // reset base uri
@ -1941,20 +1952,20 @@ func (c *DefaultCtx) getBody() []byte {
} }
// Methods to use with next stack. // Methods to use with next stack.
func (c *DefaultCtx) getMethodINT() int { func (c *DefaultCtx) getMethodInt() int {
return c.methodINT return c.methodInt
} }
func (c *DefaultCtx) getIndexRoute() int { 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

@ -339,9 +339,9 @@ type CtxGeneric[T any] interface {
release() release()
getBody() []byte getBody() []byte
// 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
@ -349,11 +349,47 @@ type CtxGeneric[T any] interface {
setIndexHandler(handler int) setIndexHandler(handler int)
setIndexRoute(route int) setIndexRoute(route int)
setMatched(matched bool) setMatched(matched bool)
setRoute(route *Route[T]) setRoute(route *Route)
// Drop closes the underlying connection without sending any response headers or body. }
// This can be useful for silently terminating client connections, such as in DDoS mitigation
// or when blocking access to sensitive endpoints. func NewDefaultCtx(app *App) *DefaultCtx {
Drop() error // return ctx
// End immediately flushes the current response and closes the underlying connection. ctx := &DefaultCtx{
End() error // Set app reference
app: app,
}
ctx.req = &DefaultReq{ctx: ctx}
ctx.res = &DefaultRes{ctx: ctx}
return ctx
}
func (app *App) newCtx() Ctx {
var c Ctx
if app.newCtxFunc != nil {
c = app.newCtxFunc(app)
} else {
c = NewDefaultCtx(app)
}
return c
}
// AcquireCtx retrieves a new Ctx from the pool.
func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) Ctx {
ctx, ok := app.pool.Get().(Ctx)
if !ok {
panic(errors.New("failed to type-assert to Ctx"))
}
ctx.Reset(fctx)
return ctx
}
// ReleaseCtx releases the ctx back into the pool.
func (app *App) ReleaseCtx(c Ctx) {
c.release()
app.pool.Put(c)
} }

View File

@ -46,7 +46,7 @@ func Test_Ctx_Accepts(t *testing.T) {
c.Request().Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9") c.Request().Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9")
require.Equal(t, "", c.Accepts("")) require.Equal(t, "", c.Accepts(""))
require.Equal(t, "", c.Accepts()) require.Equal(t, "", c.Req().Accepts())
require.Equal(t, ".xml", c.Accepts(".xml")) require.Equal(t, ".xml", c.Accepts(".xml"))
require.Equal(t, "", c.Accepts(".john")) require.Equal(t, "", c.Accepts(".john"))
require.Equal(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type") require.Equal(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type")
@ -57,13 +57,13 @@ func Test_Ctx_Accepts(t *testing.T) {
c.Request().Header.Set(HeaderAccept, "text/*, application/json") c.Request().Header.Set(HeaderAccept, "text/*, application/json")
require.Equal(t, "html", c.Accepts("html")) require.Equal(t, "html", c.Accepts("html"))
require.Equal(t, "text/html", c.Accepts("text/html")) require.Equal(t, "text/html", c.Accepts("text/html"))
require.Equal(t, "json", c.Accepts("json", "text")) require.Equal(t, "json", c.Req().Accepts("json", "text"))
require.Equal(t, "application/json", c.Accepts("application/json")) require.Equal(t, "application/json", c.Accepts("application/json"))
require.Equal(t, "", c.Accepts("image/png")) require.Equal(t, "", c.Accepts("image/png"))
require.Equal(t, "", c.Accepts("png")) require.Equal(t, "", c.Accepts("png"))
c.Request().Header.Set(HeaderAccept, "text/html, application/json") c.Request().Header.Set(HeaderAccept, "text/html, application/json")
require.Equal(t, "text/*", c.Accepts("text/*")) require.Equal(t, "text/*", c.Req().Accepts("text/*"))
c.Request().Header.Set(HeaderAccept, "*/*") c.Request().Header.Set(HeaderAccept, "*/*")
require.Equal(t, "html", c.Accepts("html")) require.Equal(t, "html", c.Accepts("html"))
@ -192,7 +192,7 @@ func Test_Ctx_AcceptsCharsets(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4
func Benchmark_Ctx_AcceptsCharsets(b *testing.B) { func Benchmark_Ctx_AcceptsCharsets(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5") c.Request().Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5")
var res string var res string
@ -218,7 +218,7 @@ func Test_Ctx_AcceptsEncodings(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4
func Benchmark_Ctx_AcceptsEncodings(b *testing.B) { func Benchmark_Ctx_AcceptsEncodings(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5")
var res string var res string
@ -243,7 +243,7 @@ func Test_Ctx_AcceptsLanguages(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4
func Benchmark_Ctx_AcceptsLanguages(b *testing.B) { func Benchmark_Ctx_AcceptsLanguages(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5")
var res string var res string
@ -304,7 +304,7 @@ func Test_Ctx_Append(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4
func Benchmark_Ctx_Append(b *testing.B) { func Benchmark_Ctx_Append(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -337,7 +337,7 @@ func Test_Ctx_Attachment(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4
func Benchmark_Ctx_Attachment(b *testing.B) { func Benchmark_Ctx_Attachment(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -363,7 +363,7 @@ func Test_Ctx_BaseURL(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem // go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem
func Benchmark_Ctx_BaseURL(b *testing.B) { func Benchmark_Ctx_BaseURL(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetHost("google.com:1337") c.Request().SetHost("google.com:1337")
c.Request().URI().SetPath("/haha/oke/lol") c.Request().URI().SetPath("/haha/oke/lol")
@ -380,7 +380,7 @@ func Benchmark_Ctx_BaseURL(b *testing.B) {
func Test_Ctx_Body(t *testing.T) { func Test_Ctx_Body(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBody([]byte("john=doe")) c.Request().SetBody([]byte("john=doe"))
require.Equal(t, []byte("john=doe"), c.Body()) require.Equal(t, []byte("john=doe"), c.Body())
@ -390,7 +390,7 @@ func Test_Ctx_Body(t *testing.T) {
func Test_Ctx_BodyRaw(t *testing.T) { func Test_Ctx_BodyRaw(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBodyRaw([]byte("john=doe")) c.Request().SetBodyRaw([]byte("john=doe"))
require.Equal(t, []byte("john=doe"), c.BodyRaw()) require.Equal(t, []byte("john=doe"), c.BodyRaw())
@ -400,7 +400,7 @@ func Test_Ctx_BodyRaw(t *testing.T) {
func Test_Ctx_BodyRaw_Immutable(t *testing.T) { func Test_Ctx_BodyRaw_Immutable(t *testing.T) {
t.Parallel() t.Parallel()
app := New(Config{Immutable: true}) app := New(Config{Immutable: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBodyRaw([]byte("john=doe")) c.Request().SetBodyRaw([]byte("john=doe"))
require.Equal(t, []byte("john=doe"), c.BodyRaw()) require.Equal(t, []byte("john=doe"), c.BodyRaw())
@ -411,7 +411,7 @@ func Benchmark_Ctx_Body(b *testing.B) {
const input = "john=doe" const input = "john=doe"
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBody([]byte(input)) c.Request().SetBody([]byte(input))
b.ReportAllocs() b.ReportAllocs()
@ -428,7 +428,7 @@ func Benchmark_Ctx_BodyRaw(b *testing.B) {
const input = "john=doe" const input = "john=doe"
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBodyRaw([]byte(input)) c.Request().SetBodyRaw([]byte(input))
b.ReportAllocs() b.ReportAllocs()
@ -445,7 +445,7 @@ func Benchmark_Ctx_BodyRaw_Immutable(b *testing.B) {
const input = "john=doe" const input = "john=doe"
app := New(Config{Immutable: true}) app := New(Config{Immutable: true})
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBodyRaw([]byte(input)) c.Request().SetBodyRaw([]byte(input))
b.ReportAllocs() b.ReportAllocs()
@ -462,7 +462,7 @@ func Test_Ctx_Body_Immutable(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
app.config.Immutable = true app.config.Immutable = true
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBody([]byte("john=doe")) c.Request().SetBody([]byte("john=doe"))
require.Equal(t, []byte("john=doe"), c.Body()) require.Equal(t, []byte("john=doe"), c.Body())
@ -474,7 +474,7 @@ func Benchmark_Ctx_Body_Immutable(b *testing.B) {
app := New() app := New()
app.config.Immutable = true app.config.Immutable = true
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().SetBody([]byte(input)) c.Request().SetBody([]byte(input))
b.ReportAllocs() b.ReportAllocs()
@ -527,7 +527,7 @@ func Test_Ctx_Body_With_Compression(t *testing.T) {
t.Run(tCase.name, func(t *testing.T) { t.Run(tCase.name, func(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set("Content-Encoding", tCase.contentEncoding) c.Request().Header.Set("Content-Encoding", tCase.contentEncoding)
if strings.Contains(tCase.contentEncoding, "gzip") { if strings.Contains(tCase.contentEncoding, "gzip") {
@ -720,7 +720,7 @@ func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
app.config.Immutable = true app.config.Immutable = true
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set("Content-Encoding", tCase.contentEncoding) c.Request().Header.Set("Content-Encoding", tCase.contentEncoding)
if strings.Contains(tCase.contentEncoding, "gzip") { if strings.Contains(tCase.contentEncoding, "gzip") {
@ -897,7 +897,7 @@ func Test_Ctx_Context(t *testing.T) {
t.Parallel() t.Parallel()
testKey := struct{}{} testKey := struct{}{}
testValue := "Test Value" testValue := "Test Value"
ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests
require.Equal(t, testValue, ctx.Value(testKey)) require.Equal(t, testValue, ctx.Value(testKey))
}) })
} }
@ -910,7 +910,7 @@ func Test_Ctx_SetContext(t *testing.T) {
testKey := struct{}{} testKey := struct{}{}
testValue := "Test Value" testValue := "Test Value"
ctx := context.WithValue(context.Background(), testKey, testValue) //nolint: staticcheck // not needed for tests ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests
c.SetContext(ctx) c.SetContext(ctx)
require.Equal(t, testValue, c.Context().Value(testKey)) require.Equal(t, testValue, c.Context().Value(testKey))
} }
@ -930,7 +930,7 @@ func Test_Ctx_Context_Multiple_Requests(t *testing.T) {
} }
input := utils.CopyString(Query(c, "input", "NO_VALUE")) input := utils.CopyString(Query(c, "input", "NO_VALUE"))
ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint: staticcheck // not needed for tests ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint:staticcheck // not needed for tests
c.SetContext(ctx) c.SetContext(ctx)
return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input)) return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input))
@ -968,52 +968,52 @@ func Test_Ctx_Cookie(t *testing.T) {
Expires: expire, Expires: expire,
// SameSite: CookieSameSiteStrictMode, // default is "lax" // SameSite: CookieSameSiteStrictMode, // default is "lax"
} }
c.Cookie(cookie) c.Res().Cookie(cookie)
expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax" expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax"
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; expires=" + httpdate + "; path=/" expect = "username=john; expires=" + httpdate + "; path=/"
cookie.SameSite = CookieSameSiteDisabled cookie.SameSite = CookieSameSiteDisabled
c.Cookie(cookie) c.Res().Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict" expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict"
cookie.SameSite = CookieSameSiteStrictMode cookie.SameSite = CookieSameSiteStrictMode
c.Cookie(cookie) c.Res().Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None" expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None"
cookie.Secure = true cookie.Secure = true
cookie.SameSite = CookieSameSiteNoneMode cookie.SameSite = CookieSameSiteNoneMode
c.Cookie(cookie) c.Res().Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; path=/; secure; SameSite=None" expect = "username=john; path=/; secure; SameSite=None"
// should remove expires and max-age headers // should remove expires and max-age headers
cookie.SessionOnly = true cookie.SessionOnly = true
cookie.Expires = expire cookie.Expires = expire
cookie.MaxAge = 10000 cookie.MaxAge = 10000
c.Cookie(cookie) c.Res().Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; path=/; secure; SameSite=None" expect = "username=john; path=/; secure; SameSite=None"
// should remove expires and max-age headers when no expire and no MaxAge (default time) // should remove expires and max-age headers when no expire and no MaxAge (default time)
cookie.SessionOnly = false cookie.SessionOnly = false
cookie.Expires = time.Time{} cookie.Expires = time.Time{}
cookie.MaxAge = 0 cookie.MaxAge = 0
c.Cookie(cookie) c.Res().Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; path=/; secure; SameSite=None; Partitioned" expect = "username=john; path=/; secure; SameSite=None; Partitioned"
cookie.Partitioned = true cookie.Partitioned = true
c.Cookie(cookie) c.Res().Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
} }
// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4
func Benchmark_Ctx_Cookie(b *testing.B) { func Benchmark_Ctx_Cookie(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -1033,8 +1033,8 @@ func Test_Ctx_Cookies(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{}) c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set("Cookie", "john=doe") c.Request().Header.Set("Cookie", "john=doe")
require.Equal(t, "doe", c.Cookies("john")) require.Equal(t, "doe", c.Req().Cookies("john"))
require.Equal(t, "default", c.Cookies("unknown", "default")) require.Equal(t, "default", c.Req().Cookies("unknown", "default"))
} }
// go test -run Test_Ctx_Format // go test -run Test_Ctx_Format
@ -1058,13 +1058,13 @@ func Test_Ctx_Format(t *testing.T) {
} }
c.Request().Header.Set(HeaderAccept, `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`) c.Request().Header.Set(HeaderAccept, `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7`)
err := c.Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...) err := c.Res().Format(formatHandlers("application/xhtml+xml", "application/xml", "foo/bar")...)
require.Equal(t, "application/xhtml+xml", accepted) require.Equal(t, "application/xhtml+xml", accepted)
require.Equal(t, "application/xhtml+xml", c.GetRespHeader(HeaderContentType)) require.Equal(t, "application/xhtml+xml", c.GetRespHeader(HeaderContentType))
require.NoError(t, err) require.NoError(t, err)
require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode()) require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode())
err = c.Format(formatHandlers("foo/bar;a=b")...) err = c.Res().Format(formatHandlers("foo/bar;a=b")...)
require.Equal(t, "foo/bar;a=b", accepted) require.Equal(t, "foo/bar;a=b", accepted)
require.Equal(t, "foo/bar;a=b", c.GetRespHeader(HeaderContentType)) require.Equal(t, "foo/bar;a=b", c.GetRespHeader(HeaderContentType))
require.NoError(t, err) require.NoError(t, err)
@ -1165,7 +1165,7 @@ func Test_Ctx_AutoFormat(t *testing.T) {
require.Equal(t, "Hello, World!", string(c.Response().Body())) require.Equal(t, "Hello, World!", string(c.Response().Body()))
c.Request().Header.Set(HeaderAccept, MIMETextHTML) c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("Hello, World!") err = c.Res().AutoFormat("Hello, World!")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "<p>Hello, World!</p>", string(c.Response().Body())) require.Equal(t, "<p>Hello, World!</p>", string(c.Response().Body()))
@ -1175,7 +1175,7 @@ func Test_Ctx_AutoFormat(t *testing.T) {
require.Equal(t, `"Hello, World!"`, string(c.Response().Body())) require.Equal(t, `"Hello, World!"`, string(c.Response().Body()))
c.Request().Header.Set(HeaderAccept, MIMETextPlain) c.Request().Header.Set(HeaderAccept, MIMETextPlain)
err = c.AutoFormat(complex(1, 1)) err = c.Res().AutoFormat(complex(1, 1))
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "(1+1i)", string(c.Response().Body())) require.Equal(t, "(1+1i)", string(c.Response().Body()))
@ -1544,12 +1544,12 @@ func Test_Ctx_Binders(t *testing.T) {
t.Run("URI", func(t *testing.T) { t.Run("URI", func(t *testing.T) {
t.Skip("URI is not ready for v3") t.Skip("URI is not ready for v3")
//nolint:gocritic // TODO: uncomment //nolint:gocritic // TODO: uncomment
//t.Parallel() // t.Parallel()
//withValues(t, func(c Ctx, testStruct *TestStruct) error { // withValues(t, func(c Ctx, testStruct *TestStruct) error {
// c.Route().Params = []string{"name", "name2", "class", "class2"} // c.Route().Params = []string{"name", "name2", "class", "class2"}
// c.Params().value = [30]string{"foo", "bar", "111", "222"} // c.Params().value = [30]string{"foo", "bar", "111", "222"}
// return c.Bind().URI(testStruct) // return c.Bind().URI(testStruct)
//}) // })
}) })
t.Run("ReqHeader", func(t *testing.T) { t.Run("ReqHeader", func(t *testing.T) {
t.Parallel() t.Parallel()
@ -2566,7 +2566,7 @@ func Test_Ctx_Params_Case_Sensitive(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4
func Benchmark_Ctx_Params(b *testing.B) { func Benchmark_Ctx_Params(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.route = &Route{ c.route = &Route{
Params: []string{ Params: []string{
@ -2939,7 +2939,7 @@ func Test_Ctx_SaveFile(t *testing.T) {
app := New() app := New()
app.Post("/test", func(c Ctx) error { app.Post("/test", func(c Ctx) error {
fh, err := c.FormFile("file") fh, err := c.Req().FormFile("file")
require.NoError(t, err) require.NoError(t, err)
tempFile, err := os.CreateTemp(os.TempDir(), "test-") tempFile, err := os.CreateTemp(os.TempDir(), "test-")
@ -3075,7 +3075,7 @@ func Test_Ctx_ClearCookie(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{}) c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set(HeaderCookie, "john=doe") c.Request().Header.Set(HeaderCookie, "john=doe")
c.ClearCookie("john") c.Res().ClearCookie("john")
require.True(t, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), "john=; expires=")) require.True(t, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), "john=; expires="))
c.Request().Header.Set(HeaderCookie, "test1=dummy") c.Request().Header.Set(HeaderCookie, "test1=dummy")
@ -3104,7 +3104,7 @@ func Test_Ctx_Download(t *testing.T) {
require.Equal(t, expect, c.Response().Body()) require.Equal(t, expect, c.Response().Body())
require.Equal(t, `attachment; filename="Awesome+File%21"`, string(c.Response().Header.Peek(HeaderContentDisposition))) require.Equal(t, `attachment; filename="Awesome+File%21"`, string(c.Response().Header.Peek(HeaderContentDisposition)))
require.NoError(t, c.Download("ctx.go")) require.NoError(t, c.Res().Download("ctx.go"))
require.Equal(t, `attachment; filename="ctx.go"`, string(c.Response().Header.Peek(HeaderContentDisposition))) require.Equal(t, `attachment; filename="ctx.go"`, string(c.Response().Header.Peek(HeaderContentDisposition)))
} }
@ -3136,7 +3136,7 @@ func Test_Ctx_SendFile(t *testing.T) {
// test with custom error code // test with custom error code
c = app.AcquireCtx(&fasthttp.RequestCtx{}) c = app.AcquireCtx(&fasthttp.RequestCtx{})
err = c.Status(StatusInternalServerError).SendFile("ctx.go") err = c.Res().Status(StatusInternalServerError).SendFile("ctx.go")
// check expectation // check expectation
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectFileContent, c.Response().Body()) require.Equal(t, expectFileContent, c.Response().Body())
@ -3161,7 +3161,7 @@ func Test_Ctx_SendFile_ContentType(t *testing.T) {
// 1) simple case // 1) simple case
c := app.AcquireCtx(&fasthttp.RequestCtx{}) c := app.AcquireCtx(&fasthttp.RequestCtx{})
err := c.SendFile("./.github/testdata/fs/img/fiber.png") err := c.Res().SendFile("./.github/testdata/fs/img/fiber.png")
// check expectation // check expectation
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, StatusOK, c.Response().StatusCode()) require.Equal(t, StatusOK, c.Response().StatusCode())
@ -3746,7 +3746,7 @@ func Benchmark_Ctx_CBOR(b *testing.B) {
func Benchmark_Ctx_JSON_Ctype(b *testing.B) { func Benchmark_Ctx_JSON_Ctype(b *testing.B) {
app := New() app := New()
// TODO: Check extra allocs because of the interface stuff // TODO: Check extra allocs because of the interface stuff
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
type SomeStruct struct { type SomeStruct struct {
Name string Name string
Age uint8 Age uint8
@ -3782,7 +3782,7 @@ func Test_Ctx_JSONP(t *testing.T) {
require.Equal(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) require.Equal(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Response().Body()))
require.Equal(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) require.Equal(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type")))
err = c.JSONP(Map{ err = c.Res().JSONP(Map{
"Name": "Grame", "Name": "Grame",
"Age": 20, "Age": 20,
}, "john") }, "john")
@ -3813,7 +3813,7 @@ func Test_Ctx_JSONP(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4
func Benchmark_Ctx_JSONP(b *testing.B) { func Benchmark_Ctx_JSONP(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
type SomeStruct struct { type SomeStruct struct {
Name string Name string
@ -3838,7 +3838,7 @@ func Benchmark_Ctx_JSONP(b *testing.B) {
func Test_Ctx_XML(t *testing.T) { func Test_Ctx_XML(t *testing.T) {
t.Parallel() t.Parallel()
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
require.Error(t, c.JSON(complex(1, 1))) require.Error(t, c.JSON(complex(1, 1)))
@ -3897,7 +3897,7 @@ func Test_Ctx_XML(t *testing.T) {
// go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4 // go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4
func Benchmark_Ctx_XML(b *testing.B) { func Benchmark_Ctx_XML(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
type SomeStruct struct { type SomeStruct struct {
Name string `xml:"Name"` Name string `xml:"Name"`
Age uint8 `xml:"Age"` Age uint8 `xml:"Age"`
@ -3936,7 +3936,7 @@ func Test_Ctx_Links(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4
func Benchmark_Ctx_Links(b *testing.B) { func Benchmark_Ctx_Links(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -4006,7 +4006,7 @@ func Test_Ctx_Render(t *testing.T) {
err = c.Render("./.github/testdata/template-non-exists.html", nil) err = c.Render("./.github/testdata/template-non-exists.html", nil)
require.Error(t, err) require.Error(t, err)
err = c.Render("./.github/testdata/template-invalid.html", nil) err = c.Res().Render("./.github/testdata/template-invalid.html", nil)
require.Error(t, err) require.Error(t, err)
} }
@ -4363,7 +4363,7 @@ func Benchmark_Ctx_Render_Engine(b *testing.B) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4
func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) { func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
app.Get("/user/:name", func(c Ctx) error { app.Get("/user/:name", func(c Ctx) error {
return c.SendString(c.Params("name")) return c.SendString(c.Params("name"))
@ -4578,14 +4578,14 @@ func Test_Ctx_SendStreamWriter(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{}) c := app.AcquireCtx(&fasthttp.RequestCtx{})
err := c.SendStreamWriter(func(w *bufio.Writer) { err := c.SendStreamWriter(func(w *bufio.Writer) {
w.WriteString("Don't crash please") //nolint:errcheck, revive // It is fine to ignore the error w.WriteString("Don't crash please") //nolint:errcheck // It is fine to ignore the error
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "Don't crash please", string(c.Response().Body())) require.Equal(t, "Don't crash please", string(c.Response().Body()))
err = c.SendStreamWriter(func(w *bufio.Writer) { err = c.SendStreamWriter(func(w *bufio.Writer) {
for lineNum := 1; lineNum <= 5; lineNum++ { for lineNum := 1; lineNum <= 5; lineNum++ {
fmt.Fprintf(w, "Line %d\n", lineNum) //nolint:errcheck, revive // It is fine to ignore the error fmt.Fprintf(w, "Line %d\n", lineNum)
if err := w.Flush(); err != nil { if err := w.Flush(); err != nil {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
return return
@ -4607,7 +4607,7 @@ func Test_Ctx_SendStreamWriter_Interrupted(t *testing.T) {
app.Get("/", func(c Ctx) error { app.Get("/", func(c Ctx) error {
return c.SendStreamWriter(func(w *bufio.Writer) { return c.SendStreamWriter(func(w *bufio.Writer) {
for lineNum := 1; lineNum <= 5; lineNum++ { for lineNum := 1; lineNum <= 5; lineNum++ {
fmt.Fprintf(w, "Line %d\n", lineNum) //nolint:errcheck // It is fine to ignore the error fmt.Fprintf(w, "Line %d\n", lineNum)
if err := w.Flush(); err != nil { if err := w.Flush(); err != nil {
if lineNum < 3 { if lineNum < 3 {
@ -4728,7 +4728,7 @@ func Benchmark_Ctx_Type(b *testing.B) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Type_Charset -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Type_Charset -benchmem -count=4
func Benchmark_Ctx_Type_Charset(b *testing.B) { func Benchmark_Ctx_Type_Charset(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -4753,7 +4753,7 @@ func Test_Ctx_Vary(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4
func Benchmark_Ctx_Vary(b *testing.B) { func Benchmark_Ctx_Vary(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -4806,7 +4806,7 @@ func Test_Ctx_Writef(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4 // go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4
func Benchmark_Ctx_Writef(b *testing.B) { func Benchmark_Ctx_Writef(b *testing.B) {
app := New() app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
world := "World!" world := "World!"
b.ReportAllocs() b.ReportAllocs()
@ -4907,7 +4907,7 @@ func Test_Ctx_Queries(t *testing.T) {
c.Request().URI().SetQueryString("tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits") c.Request().URI().SetQueryString("tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits")
queries = c.Queries() queries = c.Req().Queries()
require.Equal(t, "apple,orange,banana", queries["tags"]) require.Equal(t, "apple,orange,banana", queries["tags"])
require.Equal(t, "apple,orange,banana", queries["filters[tags]"]) require.Equal(t, "apple,orange,banana", queries["filters[tags]"])
require.Equal(t, "fruits", queries["filters[category][name]"]) require.Equal(t, "fruits", queries["filters[category][name]"])
@ -4951,11 +4951,11 @@ func Test_Ctx_BodyStreamWriter(t *testing.T) {
ctx := &fasthttp.RequestCtx{} ctx := &fasthttp.RequestCtx{}
ctx.SetBodyStreamWriter(func(w *bufio.Writer) { ctx.SetBodyStreamWriter(func(w *bufio.Writer) {
fmt.Fprintf(w, "body writer line 1\n") //nolint: errcheck // It is fine to ignore the error fmt.Fprintf(w, "body writer line 1\n")
if err := w.Flush(); err != nil { if err := w.Flush(); err != nil {
t.Errorf("unexpected error: %s", err) t.Errorf("unexpected error: %s", err)
} }
fmt.Fprintf(w, "body writer line 2\n") //nolint: errcheck // It is fine to ignore the error fmt.Fprintf(w, "body writer line 2\n")
}) })
require.True(t, ctx.IsBodyStream()) require.True(t, ctx.IsBodyStream())
@ -5055,7 +5055,7 @@ func Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{}) c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90") c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90")
require.False(t, c.IsFromLocal()) require.False(t, c.Req().IsFromLocal())
} }
} }
@ -5088,8 +5088,8 @@ func Test_Ctx_IsFromLocal_RemoteAddr(t *testing.T) {
fastCtx := &fasthttp.RequestCtx{} fastCtx := &fasthttp.RequestCtx{}
fastCtx.SetRemoteAddr(localIPv6) fastCtx.SetRemoteAddr(localIPv6)
c := app.AcquireCtx(fastCtx) c := app.AcquireCtx(fastCtx)
require.Equal(t, "::1", c.IP()) require.Equal(t, "::1", c.Req().IP())
require.True(t, c.IsFromLocal()) require.True(t, c.Req().IsFromLocal())
} }
// Test for the case fasthttp remoteAddr is set to "0:0:0:0:0:0:0:1". // Test for the case fasthttp remoteAddr is set to "0:0:0:0:0:0:0:1".
{ {

View File

@ -0,0 +1,9 @@
{
"label": "\uD83D\uDD0C Addon",
"position": 5,
"collapsed": true,
"link": {
"type": "generated-index",
"description": "Addon is an additional useful package that can be used in Fiber."
}
}

126
docs/addon/retry.md Normal file
View File

@ -0,0 +1,126 @@
---
id: retry
---
# Retry Addon
Retry addon for [Fiber](https://github.com/gofiber/fiber) designed to apply retry mechanism for unsuccessful network
operations. This addon uses an exponential backoff algorithm with jitter. It calls the function multiple times and tries
to make it successful. If all calls are failed, then, it returns an error. It adds a jitter at each retry step because adding
a jitter is a way to break synchronization across the client and avoid collision.
## Table of Contents
- [Retry Addon](#retry-addon)
- [Table of Contents](#table-of-contents)
- [Signatures](#signatures)
- [Examples](#examples)
- [Default Config](#default-config)
- [Custom Config](#custom-config)
- [Config](#config)
- [Default Config Example](#default-config-example)
## Signatures
```go
func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff
```
## Examples
```go
package main
import (
"fmt"
"github.com/gofiber/fiber/v3/addon/retry"
"github.com/gofiber/fiber/v3/client"
)
func main() {
expBackoff := retry.NewExponentialBackoff(retry.Config{})
// Local variables that will be used inside of Retry
var resp *client.Response
var err error
// Retry a network request and return an error to signify to try again
err = expBackoff.Retry(func() error {
client := client.New()
resp, err = client.Get("https://gofiber.io")
if err != nil {
return fmt.Errorf("GET gofiber.io failed: %w", err)
}
if resp.StatusCode() != 200 {
return fmt.Errorf("GET gofiber.io did not return OK 200")
}
return nil
})
// If all retries failed, panic
if err != nil {
panic(err)
}
fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode())
}
```
## Default Config
```go
retry.NewExponentialBackoff()
```
## Custom Config
```go
retry.NewExponentialBackoff(retry.Config{
InitialInterval: 2 * time.Second,
MaxBackoffTime: 64 * time.Second,
Multiplier: 2.0,
MaxRetryCount: 15,
})
```
## Config
```go
// Config defines the config for addon.
type Config struct {
// InitialInterval defines the initial time interval for backoff algorithm.
//
// Optional. Default: 1 * time.Second
InitialInterval time.Duration
// MaxBackoffTime defines maximum time duration for backoff algorithm. When
// the algorithm is reached this time, rest of the retries will be maximum
// 32 seconds.
//
// Optional. Default: 32 * time.Second
MaxBackoffTime time.Duration
// Multiplier defines multiplier number of the backoff algorithm.
//
// Optional. Default: 2.0
Multiplier float64
// MaxRetryCount defines maximum retry count for the backoff algorithm.
//
// Optional. Default: 10
MaxRetryCount int
}
```
## Default Config Example
```go
// DefaultConfig is the default config for retry.
var DefaultConfig = Config{
InitialInterval: 1 * time.Second,
MaxBackoffTime: 32 * time.Second,
Multiplier: 2.0,
MaxRetryCount: 10,
currentInterval: 1 * time.Second,
}
```

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"label": "\uD83C\uDF0E Client", "label": "\uD83C\uDF0E Client",
"position": 5, "position": 6,
"link": { "link": {
"type": "generated-index", "type": "generated-index",
"description": "HTTP client for Fiber." "description": "HTTP client for Fiber."

View File

@ -1,6 +1,6 @@
{ {
"label": "\uD83E\uDDE9 Extra", "label": "\uD83E\uDDE9 Extra",
"position": 6, "position": 8,
"link": { "link": {
"type": "generated-index", "type": "generated-index",
"description": "Extra contents for Fiber." "description": "Extra contents for Fiber."

View File

@ -1,6 +1,6 @@
{ {
"label": "\uD83D\uDCD6 Guide", "label": "\uD83D\uDCD6 Guide",
"position": 5, "position": 7,
"link": { "link": {
"type": "generated-index", "type": "generated-index",
"description": "Guides for Fiber." "description": "Guides for Fiber."

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

@ -54,7 +54,7 @@ curl -I http://localhost:3000
| ContentSecurityPolicy | `string` | ContentSecurityPolicy | "" | | ContentSecurityPolicy | `string` | ContentSecurityPolicy | "" |
| CSPReportOnly | `bool` | CSPReportOnly | false | | CSPReportOnly | `bool` | CSPReportOnly | false |
| HSTSPreloadEnabled | `bool` | HSTSPreloadEnabled | false | | HSTSPreloadEnabled | `bool` | HSTSPreloadEnabled | false |
| ReferrerPolicy | `string` | ReferrerPolicy | "ReferrerPolicy" | | ReferrerPolicy | `string` | ReferrerPolicy | "no-referrer" |
| PermissionPolicy | `string` | Permissions-Policy | "" | | PermissionPolicy | `string` | Permissions-Policy | "" |
| CrossOriginEmbedderPolicy | `string` | Cross-Origin-Embedder-Policy | "require-corp" | | CrossOriginEmbedderPolicy | `string` | Cross-Origin-Embedder-Policy | "require-corp" |
| CrossOriginOpenerPolicy | `string` | Cross-Origin-Opener-Policy | "same-origin" | | CrossOriginOpenerPolicy | `string` | Cross-Origin-Opener-Policy | "same-origin" |

View File

@ -55,13 +55,13 @@ app.Use(logger.New(logger.Config{
})) }))
// Custom File Writer // Custom File Writer
file, err := os.OpenFile("./123.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) accessLog, err := os.OpenFile("./access.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil { if err != nil {
log.Fatalf("error opening file: %v", err) log.Fatalf("error opening access.log file: %v", err)
} }
defer file.Close() defer accessLog.Close()
app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{
Output: file, Stream: accessLog,
})) }))
// Add Custom Tags // Add Custom Tags
@ -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
@ -115,7 +132,7 @@ func main() {
// Use the logger middleware with zerolog logger // Use the logger middleware with zerolog logger
app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{
Output: logger.LoggerToWriter(zap, log.LevelDebug), Stream: logger.LoggerToWriter(zap, log.LevelDebug),
})) }))
// Define a route // Define a route
@ -129,7 +146,7 @@ func main() {
``` ```
:::tip :::tip
Writing to os.File is goroutine-safe, but if you are using a custom Output that is not goroutine-safe, make sure to implement locking to properly serialize writes. Writing to os.File is goroutine-safe, but if you are using a custom Stream that is not goroutine-safe, make sure to implement locking to properly serialize writes.
::: :::
## Config ## Config
@ -137,37 +154,49 @@ Writing to os.File is goroutine-safe, but if you are using a custom Output 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` |
| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `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` | | 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` |
| Output | `io.Writer` | Output 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` |
| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - |
| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - |
| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - |
## Default Config ## Default Config
```go ```go
var ConfigDefault = Config{ var ConfigDefault = Config{
Next: nil, Next: 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,
Output: 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

@ -33,6 +33,7 @@ Here's a quick overview of the changes in Fiber `v3`:
- [Filesystem](#filesystem) - [Filesystem](#filesystem)
- [Monitor](#monitor) - [Monitor](#monitor)
- [Healthcheck](#healthcheck) - [Healthcheck](#healthcheck)
- [🔌 Addons](#-addons)
- [📋 Migration guide](#-migration-guide) - [📋 Migration guide](#-migration-guide)
## Drop for old Go versions ## Drop for old Go versions
@ -915,6 +916,47 @@ func main() {
</details> </details>
The `Skip` is a function to determine if logging is skipped or written to `Stream`.
<details>
<summary>Example Usage</summary>
```go
app.Use(logger.New(logger.Config{
Skip: func(c fiber.Ctx) bool {
// Skip logging HTTP 200 requests
return c.Response().StatusCode() == fiber.StatusOK
},
}))
```
```go
app.Use(logger.New(logger.Config{
Skip: func(c fiber.Ctx) bool {
// Only log errors, similar to an error.log
return c.Response().StatusCode() < 400
},
}))
```
</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.
@ -943,6 +985,59 @@ The Healthcheck middleware has been enhanced to support more than two routes, wi
Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes. Refer to the [healthcheck middleware migration guide](./middleware/healthcheck.md) or the [general migration guide](#-migration-guide) to review the changes.
## 🔌 Addons
In v3, Fiber introduced Addons. Addons are additional useful packages that can be used in Fiber.
### Retry
The Retry addon is a new addon that implements a retry mechanism for unsuccessful network operations. It uses an exponential backoff algorithm with jitter.
It calls the function multiple times and tries to make it successful. If all calls are failed, then, it returns an error.
It adds a jitter at each retry step because adding a jitter is a way to break synchronization across the client and avoid collision.
<details>
<summary>Example</summary>
```go
package main
import (
"fmt"
"github.com/gofiber/fiber/v3/addon/retry"
"github.com/gofiber/fiber/v3/client"
)
func main() {
expBackoff := retry.NewExponentialBackoff(retry.Config{})
// Local variables that will be used inside of Retry
var resp *client.Response
var err error
// Retry a network request and return an error to signify to try again
err = expBackoff.Retry(func() error {
client := client.New()
resp, err = client.Get("https://gofiber.io")
if err != nil {
return fmt.Errorf("GET gofiber.io failed: %w", err)
}
if resp.StatusCode() != 200 {
return fmt.Errorf("GET gofiber.io did not return OK 200")
}
return nil
})
// If all retries failed, panic
if err != nil {
panic(err)
}
fmt.Printf("GET gofiber.io succeeded with status code %d\n", resp.StatusCode())
}
```
</details>
## 📋 Migration guide ## 📋 Migration guide
- [🚀 App](#-app-1) - [🚀 App](#-app-1)

View File

@ -40,7 +40,7 @@ var (
ErrNoHandlers = errors.New("format: at least one handler is required, but none were set") ErrNoHandlers = errors.New("format: at least one handler is required, but none were set")
) )
// gorilla/schema errors // gofiber/schema errors
type ( type (
// ConversionError Conversion error exposes the internal schema.ConversionError for public use. // ConversionError Conversion error exposes the internal schema.ConversionError for public use.
ConversionError = schema.ConversionError ConversionError = schema.ConversionError

8
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/tinylib/msgp v1.2.5 github.com/tinylib/msgp v1.2.5
github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/bytebufferpool v1.0.0
github.com/valyala/fasthttp v1.59.0 github.com/valyala/fasthttp v1.59.0
golang.org/x/crypto v0.35.0 golang.org/x/crypto v0.36.0
) )
require ( require (
@ -23,8 +23,8 @@ require (
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

14
go.sum
View File

@ -32,15 +32,17 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -14,6 +14,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -51,16 +52,16 @@ func getTLSConfig(ln net.Listener) *tls.Config {
} }
// Copy value from pointer // Copy value from pointer
if val := reflect.Indirect(pointer); val.Type() != nil { if val := reflect.Indirect(pointer); val.IsValid() {
// Get private field from value // Get private field from value
if field := val.FieldByName("config"); field.Type() != nil { if field := val.FieldByName("config"); field.IsValid() {
// Copy value from pointer field (unsafe) // Copy value from pointer field (unsafe)
newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())) //nolint:gosec // Probably the only way to extract the *tls.Config from a net.Listener. TODO: Verify there really is no easier way without using unsafe. newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())) //nolint:gosec // Probably the only way to extract the *tls.Config from a net.Listener. TODO: Verify there really is no easier way without using unsafe.
if newval.Type() == nil { if !newval.IsValid() {
return nil return nil
} }
// Get element from pointer // Get element from pointer
if elem := newval.Elem(); elem.Type() != nil { if elem := newval.Elem(); elem.IsValid() {
// Cast value to *tls.Config // Cast value to *tls.Config
c, ok := elem.Interface().(*tls.Config) c, ok := elem.Interface().(*tls.Config)
if !ok { if !ok {
@ -107,15 +108,15 @@ func (app *App[TCtx]) methodExist(c *DefaultCtx) bool {
methods := app.config.RequestMethods methods := app.config.RequestMethods
for i := 0; i < len(methods); i++ { for i := 0; i < len(methods); i++ {
// Skip original method // Skip original method
if c.getMethodINT() == i { if c.getMethodInt() == i {
continue continue
} }
// Reset stack index // Reset stack index
c.setIndexRoute(-1) c.setIndexRoute(-1)
tree, ok := app.treeStack[i][c.getTreePath()] tree, ok := app.treeStack[i][c.treePathHash]
if !ok { if !ok {
tree = app.treeStack[i][""] tree = app.treeStack[i][0]
} }
// Get stack length // Get stack length
lenr := len(tree) - 1 lenr := len(tree) - 1
@ -152,15 +153,15 @@ func (app *App[TCtx]) methodExistCustom(c CustomCtx[TCtx]) bool {
methods := app.config.RequestMethods methods := app.config.RequestMethods
for i := 0; i < len(methods); i++ { for i := 0; i < len(methods); i++ {
// Skip original method // Skip original method
if c.getMethodINT() == i { if c.getMethodInt() == i {
continue continue
} }
// Reset stack index // Reset stack index
c.setIndexRoute(-1) c.setIndexRoute(-1)
tree, ok := app.treeStack[i][c.getTreePath()] tree, ok := app.treeStack[i][c.getTreePathHash()]
if !ok { if !ok {
tree = app.treeStack[i][""] tree = app.treeStack[i][0]
} }
// Get stack length // Get stack length
lenr := len(tree) - 1 lenr := len(tree) - 1
@ -484,7 +485,7 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
if len(acceptedTypes) > 1 { if len(acceptedTypes) > 1 {
// Sort accepted types by quality and specificity, preserving order of equal elements // Sort accepted types by quality and specificity, preserving order of equal elements
sortAcceptedTypes(&acceptedTypes) sortAcceptedTypes(acceptedTypes)
} }
// Find the first offer that matches the accepted types // Find the first offer that matches the accepted types
@ -512,19 +513,14 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
// A type with parameters has higher priority than an equivalent one without parameters. // A type with parameters has higher priority than an equivalent one without parameters.
// e.g., text/html;a=1;b=2 comes before text/html;a=1 // e.g., text/html;a=1;b=2 comes before text/html;a=1
// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields // See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields
func sortAcceptedTypes(acceptedTypes *[]acceptedType) { func sortAcceptedTypes(at []acceptedType) {
if acceptedTypes == nil || len(*acceptedTypes) < 2 {
return
}
at := *acceptedTypes
for i := 1; i < len(at); i++ { for i := 1; i < len(at); i++ {
lo, hi := 0, i-1 lo, hi := 0, i-1
for lo <= hi { for lo <= hi {
mid := (lo + hi) / 2 mid := (lo + hi) / 2
if at[i].quality < at[mid].quality || if at[i].quality < at[mid].quality ||
(at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity) || (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity) ||
(at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity && len(at[i].params) < len(at[mid].params)) || (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) < len(at[mid].params)) ||
(at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) == len(at[mid].params) && at[i].order > at[mid].order) { (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) == len(at[mid].params) && at[i].order > at[mid].order) {
lo = mid + 1 lo = mid + 1
} else { } else {
@ -658,39 +654,35 @@ func getBytesImmutable(s string) []byte {
func (app *App[TCtx]) methodInt(s string) int { func (app *App[TCtx]) methodInt(s string) int {
// For better performance // For better performance
if len(app.configured.RequestMethods) == 0 { if len(app.configured.RequestMethods) == 0 {
// TODO: Use iota instead
switch s { switch s {
case MethodGet: case MethodGet:
return 0 return methodGet
case MethodHead: case MethodHead:
return 1 return methodHead
case MethodPost: case MethodPost:
return 2 return methodPost
case MethodPut: case MethodPut:
return 3 return methodPut
case MethodDelete: case MethodDelete:
return 4 return methodDelete
case MethodConnect: case MethodConnect:
return 5 return methodConnect
case MethodOptions: case MethodOptions:
return 6 return methodOptions
case MethodTrace: case MethodTrace:
return 7 return methodTrace
case MethodPatch: case MethodPatch:
return 8 return methodPatch
default: default:
return -1 return -1
} }
} }
// For method customization // For method customization
for i, v := range app.config.RequestMethods { return slices.Index(app.config.RequestMethods, s)
if s == v { }
return i
}
}
return -1 func (app *App) method(methodInt int) string {
return app.config.RequestMethods[methodInt]
} }
// IsMethodSafe reports whether the HTTP method is considered safe. // IsMethodSafe reports whether the HTTP method is considered safe.

View File

@ -354,7 +354,6 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) {
{spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "text/html", quality: 1, specificity: 3, order: 0},
{spec: "text/*", quality: 0.5, specificity: 2, order: 1}, {spec: "text/*", quality: 0.5, specificity: 2, order: 1},
{spec: "*/*", quality: 0.1, specificity: 1, order: 2}, {spec: "*/*", quality: 0.1, specificity: 1, order: 2},
{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
{spec: "application/xml", quality: 1, specificity: 3, order: 4}, {spec: "application/xml", quality: 1, specificity: 3, order: 4},
{spec: "application/pdf", quality: 1, specificity: 3, order: 5}, {spec: "application/pdf", quality: 1, specificity: 3, order: 5},
{spec: "image/png", quality: 1, specificity: 3, order: 6}, {spec: "image/png", quality: 1, specificity: 3, order: 6},
@ -363,8 +362,9 @@ func Test_Utils_SortAcceptedTypes(t *testing.T) {
{spec: "image/gif", quality: 1, specificity: 3, order: 9}, {spec: "image/gif", quality: 1, specificity: 3, order: 9},
{spec: "text/plain", quality: 1, specificity: 3, order: 10}, {spec: "text/plain", quality: 1, specificity: 3, order: 10},
{spec: "application/json", quality: 0.999, specificity: 3, params: headerParams{"a": []byte("1")}, order: 11}, {spec: "application/json", quality: 0.999, specificity: 3, params: headerParams{"a": []byte("1")}, order: 11},
{spec: "application/json", quality: 0.999, specificity: 3, order: 3},
} }
sortAcceptedTypes(&acceptedTypes) sortAcceptedTypes(acceptedTypes)
require.Equal(t, []acceptedType{ require.Equal(t, []acceptedType{
{spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "text/html", quality: 1, specificity: 3, order: 0},
{spec: "application/xml", quality: 1, specificity: 3, order: 4}, {spec: "application/xml", quality: 1, specificity: 3, order: 4},
@ -390,7 +390,7 @@ func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) {
acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0} acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0}
acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1} acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1}
acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2}
sortAcceptedTypes(&acceptedTypes) sortAcceptedTypes(acceptedTypes)
} }
require.Equal(b, "text/html", acceptedTypes[0].spec) require.Equal(b, "text/html", acceptedTypes[0].spec)
require.Equal(b, "text/*", acceptedTypes[1].spec) require.Equal(b, "text/*", acceptedTypes[1].spec)
@ -414,7 +414,7 @@ func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) {
acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8} acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8}
acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9} acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9}
acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10} acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10}
sortAcceptedTypes(&acceptedTypes) sortAcceptedTypes(acceptedTypes)
} }
require.Equal(b, []acceptedType{ require.Equal(b, []acceptedType{
{spec: "text/html", quality: 1, specificity: 3, order: 0}, {spec: "text/html", quality: 1, specificity: 3, order: 0},
@ -566,7 +566,7 @@ func Test_Utils_TestConn_Closed_Write(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Close early, write should fail // Close early, write should fail
conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here conn.Close() //nolint:errcheck // It is fine to ignore the error here
_, err = conn.Write([]byte("Response 2\n")) _, err = conn.Write([]byte("Response 2\n"))
require.ErrorIs(t, err, errTestConnClosed) require.ErrorIs(t, err, errTestConnClosed)

View File

@ -209,7 +209,7 @@ func Benchmark_Memory_Set(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark
} }
} }
@ -220,7 +220,7 @@ func Benchmark_Memory_Set_Parallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
_ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark
} }
}) })
} }
@ -259,7 +259,7 @@ func Benchmark_Memory_Get(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, _ = testStore.Get("john") //nolint: errcheck // error not needed for benchmark _, _ = testStore.Get("john") //nolint:errcheck // error not needed for benchmark
} }
} }
@ -273,7 +273,7 @@ func Benchmark_Memory_Get_Parallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
_, _ = testStore.Get("john") //nolint: errcheck // error not needed for benchmark _, _ = testStore.Get("john") //nolint:errcheck // error not needed for benchmark
} }
}) })
} }
@ -315,8 +315,8 @@ func Benchmark_Memory_SetAndDelete(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark
_ = testStore.Delete("john") //nolint: errcheck // error not needed for benchmark _ = testStore.Delete("john") //nolint:errcheck // error not needed for benchmark
} }
} }
@ -327,8 +327,8 @@ func Benchmark_Memory_SetAndDelete_Parallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
_ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark _ = testStore.Set("john", []byte("doe"), 0) //nolint:errcheck // error not needed for benchmark
_ = testStore.Delete("john") //nolint: errcheck // error not needed for benchmark _ = testStore.Delete("john") //nolint:errcheck // error not needed for benchmark
} }
}) })
} }

View File

@ -328,7 +328,7 @@ func (*App[TCtx]) prepareListenData(addr string, isTLS bool, cfg ListenConfig[TC
} }
// startupMessage prepares the startup message with the handler number, port, address and other information // startupMessage prepares the startup message with the handler number, port, address and other information
func (app *App[TCtx]) startupMessage(addr string, isTLS bool, pids string, cfg ListenConfig[TCtx]) { //nolint: revive // Accepting a bool param named isTLS if fine here func (app *App[TCtx]) startupMessage(addr string, isTLS bool, pids string, cfg ListenConfig[TCtx]) { //nolint:revive // Accepting a bool param named isTLS if fine here
// ignore child processes // ignore child processes
if IsChild() { if IsChild() {
return return
@ -366,38 +366,35 @@ func (app *App[TCtx]) startupMessage(addr string, isTLS bool, pids string, cfg L
out = colorable.NewNonColorable(os.Stdout) out = colorable.NewNonColorable(os.Stdout)
} }
fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset)) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset))
fmt.Fprintf(out, strings.Repeat("-", 50)+"\n") //nolint:errcheck,revive,govet // ignore error fmt.Fprintf(out, strings.Repeat("-", 50)+"\n")
if host == "0.0.0.0" { if host == "0.0.0.0" {
//nolint:errcheck,revive // ignore error
fmt.Fprintf(out, fmt.Fprintf(out,
"%sINFO%s Server started on: \t%s%s://127.0.0.1:%s%s (bound on host 0.0.0.0 and port %s)\n", "%sINFO%s Server started on: \t%s%s://127.0.0.1:%s%s (bound on host 0.0.0.0 and port %s)\n",
colors.Green, colors.Reset, colors.Blue, scheme, port, colors.Reset, port) colors.Green, colors.Reset, colors.Blue, scheme, port, colors.Reset, port)
} else { } else {
//nolint:errcheck,revive // ignore error
fmt.Fprintf(out, fmt.Fprintf(out,
"%sINFO%s Server started on: \t%s%s%s\n", "%sINFO%s Server started on: \t%s%s%s\n",
colors.Green, colors.Reset, colors.Blue, fmt.Sprintf("%s://%s:%s", scheme, host, port), colors.Reset) colors.Green, colors.Reset, colors.Blue, fmt.Sprintf("%s://%s:%s", scheme, host, port), colors.Reset)
} }
if app.config.AppName != "" { if app.config.AppName != "" {
fmt.Fprintf(out, "%sINFO%s Application name: \t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, app.config.AppName, colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Application name: \t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, app.config.AppName, colors.Reset)
} }
//nolint:errcheck,revive // ignore error
fmt.Fprintf(out, fmt.Fprintf(out,
"%sINFO%s Total handlers count: \t%s%s%s\n", "%sINFO%s Total handlers count: \t%s%s%s\n",
colors.Green, colors.Reset, colors.Blue, strconv.Itoa(int(app.handlersCount)), colors.Reset) colors.Green, colors.Reset, colors.Blue, strconv.Itoa(int(app.handlersCount)), colors.Reset)
if isPrefork == "Enabled" { if isPrefork == "Enabled" {
fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, isPrefork, colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, isPrefork, colors.Reset)
} else { } else {
fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Red, isPrefork, colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Red, isPrefork, colors.Reset)
} }
fmt.Fprintf(out, "%sINFO%s PID: \t\t\t%s%v%s\n", colors.Green, colors.Reset, colors.Blue, os.Getpid(), colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s PID: \t\t\t%s%v%s\n", colors.Green, colors.Reset, colors.Blue, os.Getpid(), colors.Reset)
fmt.Fprintf(out, "%sINFO%s Total process count: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, procs, colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Total process count: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, procs, colors.Reset)
if cfg.EnablePrefork { if cfg.EnablePrefork {
// Turn the `pids` variable (in the form ",a,b,c,d,e,f,etc") into a slice of PIDs // Turn the `pids` variable (in the form ",a,b,c,d,e,f,etc") into a slice of PIDs
@ -408,7 +405,7 @@ func (app *App[TCtx]) startupMessage(addr string, isTLS bool, pids string, cfg L
} }
} }
fmt.Fprintf(out, "%sINFO%s Child PIDs: \t\t%s", colors.Green, colors.Reset, colors.Blue) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%sINFO%s Child PIDs: \t\t%s", colors.Green, colors.Reset, colors.Blue)
totalPids := len(pidSlice) totalPids := len(pidSlice)
rowTotalPidCount := 10 rowTotalPidCount := 10
@ -421,17 +418,17 @@ func (app *App[TCtx]) startupMessage(addr string, isTLS bool, pids string, cfg L
} }
for n, pid := range pidSlice[start:end] { for n, pid := range pidSlice[start:end] {
fmt.Fprintf(out, "%s", pid) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "%s", pid)
if n+1 != len(pidSlice[start:end]) { if n+1 != len(pidSlice[start:end]) {
fmt.Fprintf(out, ", ") //nolint:errcheck,revive // ignore error fmt.Fprintf(out, ", ")
} }
} }
fmt.Fprintf(out, "\n%s", colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "\n%s", colors.Reset)
} }
} }
// add new Line as spacer // add new Line as spacer
fmt.Fprintf(out, "\n%s", colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(out, "\n%s", colors.Reset)
} }
// printRoutesMessage print all routes with method, path, name and handlers // printRoutesMessage print all routes with method, path, name and handlers
@ -473,11 +470,10 @@ func (app *App[TCtx]) printRoutesMessage() {
return routes[i].path < routes[j].path return routes[i].path < routes[j].path
}) })
fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset)
fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) //nolint:errcheck,revive // ignore error fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset)
for _, route := range routes { for _, route := range routes {
//nolint:errcheck,revive // ignore error
fmt.Fprintf(w, "%s%s\t%s| %s%s\t%s| %s%s\t%s| %s%s%s\n", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers, colors.Reset) fmt.Fprintf(w, "%s%s\t%s| %s%s\t%s| %s%s\t%s| %s%s%s\n", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers, colors.Reset)
} }

View File

@ -53,9 +53,9 @@ func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []any) {
buf.WriteString(level) buf.WriteString(level)
if len(fmtArgs) > 0 { if len(fmtArgs) > 0 {
_, _ = fmt.Fprintf(buf, format, fmtArgs...) //nolint: errcheck // It is fine to ignore the error _, _ = fmt.Fprintf(buf, format, fmtArgs...)
} else { } else {
_, _ = fmt.Fprint(buf, fmtArgs...) //nolint: errcheck // It is fine to ignore the error _, _ = fmt.Fprint(buf, fmtArgs...)
} }
_ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error

View File

@ -1,4 +1,4 @@
//nolint:contextcheck, revive // Much easier to just ignore memory leaks in tests //nolint:contextcheck,revive // Much easier to just ignore memory leaks in tests
package adaptor package adaptor
import ( import (
@ -68,7 +68,7 @@ func Test_HTTPHandler(t *testing.T) {
w.Header().Set("Header1", "value1") w.Header().Set("Header1", "value1")
w.Header().Set("Header2", "value2") w.Header().Set("Header2", "value2")
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "request body is %q", body) //nolint:errcheck // not needed fmt.Fprintf(w, "request body is %q", body)
} }
fiberH := HTTPHandlerFunc(http.HandlerFunc(nethttpH)) fiberH := HTTPHandlerFunc(http.HandlerFunc(nethttpH))
fiberH = setFiberContextValueMiddleware(fiberH, expectedContextKey, expectedContextValue) fiberH = setFiberContextValueMiddleware(fiberH, expectedContextKey, expectedContextValue)

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

@ -28,7 +28,7 @@ type Config struct {
ContentSecurityPolicy string ContentSecurityPolicy string
// ReferrerPolicy // ReferrerPolicy
// Optional. Default value "ReferrerPolicy". // Optional. Default value "no-referrer".
ReferrerPolicy string ReferrerPolicy string
// Permissions-Policy // Permissions-Policy

View File

@ -2,6 +2,7 @@
package keyauth package keyauth
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
@ -59,7 +60,10 @@ func New(config ...Config) fiber.Handler {
valid, err := cfg.Validator(c, key) valid, err := cfg.Validator(c, key)
if err == nil && valid { if err == nil && valid {
// Store in both Locals and Context
c.Locals(tokenKey, key) c.Locals(tokenKey, key)
ctx := context.WithValue(c.Context(), tokenKey, key)
c.SetContext(ctx)
return cfg.SuccessHandler(c) return cfg.SuccessHandler(c)
} }
return cfg.ErrorHandler(c, err) return cfg.ErrorHandler(c, err)
@ -68,12 +72,20 @@ func New(config ...Config) fiber.Handler {
// TokenFromContext returns the bearer token from the request context. // TokenFromContext returns the bearer token from the request context.
// returns an empty string if the token does not exist // returns an empty string if the token does not exist
func TokenFromContext(c fiber.Ctx) string { func TokenFromContext(c any) string {
token, ok := c.Locals(tokenKey).(string) switch ctx := c.(type) {
if !ok { case context.Context:
return "" if token, ok := ctx.Value(tokenKey).(string); ok {
}
return token return token
}
case fiber.Ctx:
if token, ok := ctx.Locals(tokenKey).(string); ok {
return token
}
default:
panic("unsupported context type, expected fiber.Ctx or context.Context")
}
return ""
} }
// MultipleKeySourceLookup creates a CustomKeyLookup function that checks multiple sources until one is found // MultipleKeySourceLookup creates a CustomKeyLookup function that checks multiple sources until one is found

View File

@ -503,8 +503,9 @@ func Test_TokenFromContext_None(t *testing.T) {
} }
func Test_TokenFromContext(t *testing.T) { func Test_TokenFromContext(t *testing.T) {
// Test that TokenFromContext returns the correct token
t.Run("fiber.Ctx", func(t *testing.T) {
app := fiber.New() app := fiber.New()
// Wire up keyauth middleware to set TokenFromContext now
app.Use(New(Config{ app.Use(New(Config{
KeyLookup: "header:Authorization", KeyLookup: "header:Authorization",
AuthScheme: "Basic", AuthScheme: "Basic",
@ -515,21 +516,54 @@ func Test_TokenFromContext(t *testing.T) {
return false, ErrMissingOrMalformedAPIKey return false, ErrMissingOrMalformedAPIKey
}, },
})) }))
// Define a test handler that checks TokenFromContext
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString(TokenFromContext(c)) return c.SendString(TokenFromContext(c))
}) })
req := httptest.NewRequest(fiber.MethodGet, "/", nil) req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Add("Authorization", "Basic "+CorrectKey) req.Header.Add("Authorization", "Basic "+CorrectKey)
// Send
res, err := app.Test(req) res, err := app.Test(req)
require.NoError(t, err) require.NoError(t, err)
// Read the response body into a string
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, CorrectKey, string(body)) require.Equal(t, CorrectKey, string(body))
})
t.Run("context.Context", func(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
KeyLookup: "header:Authorization",
AuthScheme: "Basic",
Validator: func(_ fiber.Ctx, key string) (bool, error) {
if key == CorrectKey {
return true, nil
}
return false, ErrMissingOrMalformedAPIKey
},
}))
// Verify that TokenFromContext works with context.Context
app.Get("/", func(c fiber.Ctx) error {
ctx := c.Context()
token := TokenFromContext(ctx)
return c.SendString(token)
})
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Add("Authorization", "Basic "+CorrectKey)
res, err := app.Test(req)
require.NoError(t, err)
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, CorrectKey, string(body))
})
t.Run("invalid context type", func(t *testing.T) {
require.Panics(t, func() {
_ = TokenFromContext("invalid")
})
})
} }
func Test_AuthSchemeToken(t *testing.T) { func Test_AuthSchemeToken(t *testing.T) {

View File

@ -10,16 +10,21 @@ import (
// Config defines the config for middleware. // Config defines the config for middleware.
type Config struct { type Config struct {
// Output is a writer where logs are written // Stream is a writer where logs are written
// //
// Default: os.Stdout // Default: os.Stdout
Output io.Writer Stream io.Writer
// Next defines a function to skip this middleware when returned true. // Next defines a function to skip this middleware when returned true.
// //
// Optional. Default: nil // Optional. Default: nil
Next func(c fiber.Ctx) bool Next func(c fiber.Ctx) bool
// Skip is a function to determine if logging is skipped or written to Stream.
//
// Optional. Default: nil
Skip func(c fiber.Ctx) bool
// Done is a function that is called after the log string for a request is written to Output, // Done is a function that is called after the log string for a request is written to Output,
// and pass the log string as parameter. // and pass the log string as parameter.
// //
@ -45,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
@ -98,20 +117,18 @@ type LogFunc func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (in
// ConfigDefault is the default config // ConfigDefault is the default config
var ConfigDefault = Config{ var ConfigDefault = Config{
Next: nil, Next: 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,
Output: os.Stdout, Stream: os.Stdout,
BeforeHandlerFunc: beforeHandlerFunc, BeforeHandlerFunc: beforeHandlerFunc,
LoggerFunc: defaultLoggerInstance, LoggerFunc: defaultLoggerInstance,
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
@ -126,6 +143,9 @@ func configDefault(config ...Config) Config {
if cfg.Next == nil { if cfg.Next == nil {
cfg.Next = ConfigDefault.Next cfg.Next = ConfigDefault.Next
} }
if cfg.Skip == nil {
cfg.Skip = ConfigDefault.Skip
}
if cfg.Done == nil { if cfg.Done == nil {
cfg.Done = ConfigDefault.Done cfg.Done = ConfigDefault.Done
} }
@ -141,8 +161,8 @@ func configDefault(config ...Config) Config {
if int(cfg.TimeInterval) <= 0 { if int(cfg.TimeInterval) <= 0 {
cfg.TimeInterval = ConfigDefault.TimeInterval cfg.TimeInterval = ConfigDefault.TimeInterval
} }
if cfg.Output == nil { if cfg.Stream == nil {
cfg.Output = ConfigDefault.Output cfg.Stream = ConfigDefault.Stream
} }
if cfg.BeforeHandlerFunc == nil { if cfg.BeforeHandlerFunc == nil {
@ -154,7 +174,7 @@ func configDefault(config ...Config) Config {
} }
// Enable colors if no custom format or output is given // Enable colors if no custom format or output is given
if !cfg.DisableColors && cfg.Output == ConfigDefault.Output { if !cfg.DisableColors && cfg.Stream == ConfigDefault.Stream {
cfg.enableColors = true cfg.enableColors = true
} }

View File

@ -15,6 +15,12 @@ import (
// default logger for fiber // default logger for fiber
func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
// Check if Skip is defined and call it.
// Now, if Skip(c) == true, we SKIP logging:
if cfg.Skip != nil && cfg.Skip(c) {
return nil // Skip logging if Skip returns true
}
// Alias colors // Alias colors
colors := c.App().Config().ColorScheme colors := c.App().Config().ColorScheme
@ -22,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 {
@ -91,7 +97,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
} }
// Write buffer to output // Write buffer to output
writeLog(cfg.Output, buf.Bytes()) writeLog(cfg.Stream, buf.Bytes())
if cfg.Done != nil { if cfg.Done != nil {
cfg.Done(c, buf.Bytes()) cfg.Done(c, buf.Bytes())
@ -125,7 +131,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
buf.WriteString(err.Error()) buf.WriteString(err.Error())
} }
writeLog(cfg.Output, buf.Bytes()) writeLog(cfg.Stream, buf.Bytes())
if cfg.Done != nil { if cfg.Done != nil {
cfg.Done(c, buf.Bytes()) cfg.Done(c, buf.Bytes())
@ -141,9 +147,9 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
func beforeHandlerFunc(cfg Config) { func beforeHandlerFunc(cfg Config) {
// If colors are enabled, check terminal compatibility // If colors are enabled, check terminal compatibility
if cfg.enableColors { if cfg.enableColors {
cfg.Output = colorable.NewColorableStdout() cfg.Stream = colorable.NewColorableStdout()
if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {
cfg.Output = colorable.NewNonColorable(os.Stdout) cfg.Stream = colorable.NewNonColorable(os.Stdout)
} }
} }
} }
@ -160,7 +166,7 @@ func writeLog(w io.Writer, msg []byte) {
// Write error to output // Write error to output
if _, err := w.Write([]byte(err.Error())); err != nil { if _, err := w.Write([]byte(err.Error())); err != nil {
// There is something wrong with the given io.Writer // There is something wrong with the given io.Writer
_, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) //nolint: errcheck // It is fine to ignore the error _, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
} }
} }
} }

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

@ -71,7 +71,7 @@ func Test_Logger(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${error}", Format: "${error}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(_ fiber.Ctx) error { app.Get("/", func(_ fiber.Ctx) error {
@ -94,7 +94,7 @@ func Test_Logger_locals(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${locals:demo}", Format: "${locals:demo}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
@ -171,6 +171,147 @@ func Test_Logger_Done(t *testing.T) {
require.Positive(t, buf.Len(), 0) require.Positive(t, buf.Len(), 0)
} }
// Test_Logger_Filter tests the Filter functionality of the logger middleware.
// It verifies that logs are written or skipped based on the filter condition.
func Test_Logger_Filter(t *testing.T) {
t.Parallel()
t.Run("Test Not Found", func(t *testing.T) {
t.Parallel()
app := fiber.New()
logOutput := bytes.Buffer{}
// Return true to skip logging for all requests != 404
app.Use(New(Config{
Skip: func(c fiber.Ctx) bool {
return c.Response().StatusCode() != fiber.StatusNotFound
},
Stream: &logOutput,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/nonexistent", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
// Expect logs for the 404 request
require.Contains(t, logOutput.String(), "404")
})
t.Run("Test OK", func(t *testing.T) {
t.Parallel()
app := fiber.New()
logOutput := bytes.Buffer{}
// Return true to skip logging for all requests == 200
app.Use(New(Config{
Skip: func(c fiber.Ctx) bool {
return c.Response().StatusCode() == fiber.StatusOK
},
Stream: &logOutput,
}))
app.Get("/", 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)
// We skip logging for status == 200, so "200" should not appear
require.NotContains(t, logOutput.String(), "200")
})
t.Run("Always Skip", func(t *testing.T) {
t.Parallel()
app := fiber.New()
logOutput := bytes.Buffer{}
// Filter always returns true => skip all logs
app.Use(New(Config{
Skip: func(_ fiber.Ctx) bool {
return true // always skip
},
Stream: &logOutput,
}))
app.Get("/something", func(c fiber.Ctx) error {
return c.Status(fiber.StatusTeapot).SendString("I'm a teapot")
})
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/something", nil))
require.NoError(t, err)
// Expect NO logs
require.Empty(t, logOutput.String())
})
t.Run("Never Skip", func(t *testing.T) {
t.Parallel()
app := fiber.New()
logOutput := bytes.Buffer{}
// Filter always returns false => never skip logs
app.Use(New(Config{
Skip: func(_ fiber.Ctx) bool {
return false // never skip
},
Stream: &logOutput,
}))
app.Get("/always", func(c fiber.Ctx) error {
return c.Status(fiber.StatusTeapot).SendString("Teapot again")
})
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/always", nil))
require.NoError(t, err)
// Expect some logging - check any substring
require.Contains(t, logOutput.String(), strconv.Itoa(fiber.StatusTeapot))
})
t.Run("Skip /healthz", func(t *testing.T) {
t.Parallel()
app := fiber.New()
logOutput := bytes.Buffer{}
// Filter returns true (skip logs) if the request path is /healthz
app.Use(New(Config{
Skip: func(c fiber.Ctx) bool {
return c.Path() == "/healthz"
},
Stream: &logOutput,
}))
// Normal route
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello World!")
})
// Health route
app.Get("/healthz", func(c fiber.Ctx) error {
return c.SendString("OK")
})
// Request to "/" -> should be logged
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Contains(t, logOutput.String(), "200")
// Reset output buffer
logOutput.Reset()
// Request to "/healthz" -> should be skipped
_, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/healthz", nil))
require.NoError(t, err)
require.Empty(t, logOutput.String())
})
}
// go test -run Test_Logger_ErrorTimeZone // go test -run Test_Logger_ErrorTimeZone
func Test_Logger_ErrorTimeZone(t *testing.T) { func Test_Logger_ErrorTimeZone(t *testing.T) {
t.Parallel() t.Parallel()
@ -234,7 +375,7 @@ func Test_Logger_LoggerToWriter(t *testing.T) {
app.Use("/"+level, New(Config{ app.Use("/"+level, New(Config{
Format: "${error}", Format: "${error}",
Output: LoggerToWriter(logger, tc. Stream: LoggerToWriter(logger, tc.
level), level),
})) }))
@ -276,7 +417,7 @@ func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: o, Stream: o,
DisableColors: true, DisableColors: true,
})) }))
@ -293,7 +434,7 @@ func Test_Logger_ErrorOutput(t *testing.T) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: o, Stream: o,
})) }))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
@ -312,7 +453,7 @@ func Test_Logger_All(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}",
Output: buf, Stream: buf,
})) }))
// Alias colors // Alias colors
@ -326,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
@ -358,7 +617,7 @@ func Test_Logger_WithLatency(t *testing.T) {
app := fiber.New() app := fiber.New()
logger := New(Config{ logger := New(Config{
Output: buff, Stream: buff,
Format: "${latency}", Format: "${latency}",
}) })
app.Use(logger) app.Use(logger)
@ -403,7 +662,7 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) {
app := fiber.New() app := fiber.New()
logger := New(Config{ logger := New(Config{
Output: buff, Stream: buff,
}) })
app.Use(logger) app.Use(logger)
@ -453,7 +712,7 @@ func Test_Query_Params(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${queryParams}", Format: "${queryParams}",
Output: buf, Stream: buf,
})) }))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil)) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil))
@ -474,7 +733,7 @@ func Test_Response_Body(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${resBody}", Format: "${resBody}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
@ -508,7 +767,7 @@ func Test_Request_Body(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: buf, Stream: buf,
})) }))
app.Post("/", func(c fiber.Ctx) error { app.Post("/", func(c fiber.Ctx) error {
@ -536,7 +795,7 @@ func Test_Logger_AppendUint(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
@ -611,7 +870,7 @@ func Test_Response_Header(t *testing.T) {
})) }))
app.Use(New(Config{ app.Use(New(Config{
Format: "${respHeader:X-Request-ID}", Format: "${respHeader:X-Request-ID}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!") return c.SendString("Hello fiber!")
@ -634,7 +893,7 @@ func Test_Req_Header(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${reqHeader:test}", Format: "${reqHeader:test}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!") return c.SendString("Hello fiber!")
@ -658,7 +917,7 @@ func Test_ReqHeader_Header(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${reqHeader:test}", Format: "${reqHeader:test}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!") return c.SendString("Hello fiber!")
@ -689,7 +948,7 @@ func Test_CustomTags(t *testing.T) {
return output.WriteString(customTag) return output.WriteString(customTag)
}, },
}, },
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!") return c.SendString("Hello fiber!")
@ -713,7 +972,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) {
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: buf, Stream: buf,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
@ -724,7 +983,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) {
for { for {
i++ i++
msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
fmt.Fprintf(w, "data: Message: %s\n\n", msg) //nolint:errcheck // ignore error fmt.Fprintf(w, "data: Message: %s\n\n", msg)
err := w.Flush() err := w.Flush()
if err != nil { if err != nil {
break break
@ -759,7 +1018,7 @@ func Test_Logger_EnableColors(t *testing.T) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: o, Stream: o,
})) }))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
@ -782,7 +1041,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test") c.Set("test", "test")
@ -794,7 +1053,7 @@ func Benchmark_Logger(b *testing.B) {
b.Run("DefaultFormat", func(bb *testing.B) { b.Run("DefaultFormat", func(bb *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!") return c.SendString("Hello, World!")
@ -805,7 +1064,7 @@ func Benchmark_Logger(b *testing.B) {
b.Run("DefaultFormatDisableColors", func(bb *testing.B) { b.Run("DefaultFormatDisableColors", func(bb *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: io.Discard, Stream: io.Discard,
DisableColors: true, DisableColors: true,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
@ -819,7 +1078,7 @@ func Benchmark_Logger(b *testing.B) {
logger := fiberlog.DefaultLogger() logger := fiberlog.DefaultLogger()
logger.SetOutput(io.Discard) logger.SetOutput(io.Discard)
app.Use(New(Config{ app.Use(New(Config{
Output: LoggerToWriter(logger, fiberlog.LevelDebug), Stream: LoggerToWriter(logger, fiberlog.LevelDebug),
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!") return c.SendString("Hello, World!")
@ -831,7 +1090,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test") c.Set("test", "test")
@ -844,7 +1103,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${locals:demo}", Format: "${locals:demo}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe") c.Locals("demo", "johndoe")
@ -857,7 +1116,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${locals:demo}", Format: "${locals:demo}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/int", func(c fiber.Ctx) error { app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55) c.Locals("demo", 55)
@ -874,7 +1133,7 @@ func Benchmark_Logger(b *testing.B) {
io.Discard.Write(logString) //nolint:errcheck // ignore error io.Discard.Write(logString) //nolint:errcheck // ignore error
} }
}, },
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/logging", func(ctx fiber.Ctx) error { app.Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK) return ctx.SendStatus(fiber.StatusOK)
@ -886,7 +1145,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!") return c.SendString("Hello, World!")
@ -898,7 +1157,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive") c.Set("Connection", "keep-alive")
@ -908,7 +1167,7 @@ func Benchmark_Logger(b *testing.B) {
for { for {
i++ i++
msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
fmt.Fprintf(w, "data: Message: %s\n\n", msg) //nolint:errcheck // ignore error fmt.Fprintf(w, "data: Message: %s\n\n", msg)
err := w.Flush() err := w.Flush()
if err != nil { if err != nil {
break break
@ -927,7 +1186,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${resBody}", Format: "${resBody}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Sample response body") return c.SendString("Sample response body")
@ -950,7 +1209,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test") c.Set("test", "test")
@ -962,7 +1221,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
b.Run("DefaultFormat", func(bb *testing.B) { b.Run("DefaultFormat", func(bb *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!") return c.SendString("Hello, World!")
@ -975,7 +1234,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
logger := fiberlog.DefaultLogger() logger := fiberlog.DefaultLogger()
logger.SetOutput(io.Discard) logger.SetOutput(io.Discard)
app.Use(New(Config{ app.Use(New(Config{
Output: LoggerToWriter(logger, fiberlog.LevelDebug), Stream: LoggerToWriter(logger, fiberlog.LevelDebug),
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!") return c.SendString("Hello, World!")
@ -986,7 +1245,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
b.Run("DefaultFormatDisableColors", func(bb *testing.B) { b.Run("DefaultFormatDisableColors", func(bb *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Output: io.Discard, Stream: io.Discard,
DisableColors: true, DisableColors: true,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
@ -999,7 +1258,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test") c.Set("test", "test")
@ -1012,7 +1271,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${locals:demo}", Format: "${locals:demo}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe") c.Locals("demo", "johndoe")
@ -1025,7 +1284,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${locals:demo}", Format: "${locals:demo}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/int", func(c fiber.Ctx) error { app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55) c.Locals("demo", 55)
@ -1042,7 +1301,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
io.Discard.Write(logString) //nolint:errcheck // ignore error io.Discard.Write(logString) //nolint:errcheck // ignore error
} }
}, },
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/logging", func(ctx fiber.Ctx) error { app.Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK) return ctx.SendStatus(fiber.StatusOK)
@ -1054,7 +1313,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!") return c.SendString("Hello, World!")
@ -1066,7 +1325,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}", Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive") c.Set("Connection", "keep-alive")
@ -1076,7 +1335,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
for { for {
i++ i++
msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
fmt.Fprintf(w, "data: Message: %s\n\n", msg) //nolint:errcheck // ignore error fmt.Fprintf(w, "data: Message: %s\n\n", msg)
err := w.Flush() err := w.Flush()
if err != nil { if err != nil {
break break
@ -1095,7 +1354,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New() app := fiber.New()
app.Use(New(Config{ app.Use(New(Config{
Format: "${resBody}", Format: "${resBody}",
Output: io.Discard, Stream: io.Discard,
})) }))
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Sample response body") return c.SendString("Sample response body")

View File

@ -506,7 +506,10 @@ func Test_Proxy_Do_WithRealURL(t *testing.T) {
return Do(c, "https://www.google.com") return Do(c, "https://www.google.com")
}) })
resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{
Timeout: 2 * time.Second,
FailOnTimeout: true,
})
require.NoError(t, err1) require.NoError(t, err1)
require.Equal(t, fiber.StatusOK, resp.StatusCode) require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "/test", resp.Request.URL.String()) require.Equal(t, "/test", resp.Request.URL.String())
@ -523,7 +526,10 @@ func Test_Proxy_Do_WithRedirect(t *testing.T) {
return Do(c, "https://google.com") return Do(c, "https://google.com")
}) })
resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{
Timeout: 2 * time.Second,
FailOnTimeout: true,
})
require.NoError(t, err1) require.NoError(t, err1)
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
require.NoError(t, err) require.NoError(t, err)
@ -558,7 +564,10 @@ func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) {
return DoRedirects(c, "http://google.com", 0) return DoRedirects(c, "http://google.com", 0)
}) })
resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{
Timeout: 2 * time.Second,
FailOnTimeout: true,
})
require.NoError(t, err1) require.NoError(t, err1)
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
require.NoError(t, err) require.NoError(t, err)

129
path.go
View File

@ -7,9 +7,11 @@
package fiber package fiber
import ( import (
"bytes"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"unicode" "unicode"
@ -25,6 +27,12 @@ type routeParser struct {
plusCount int // number of plus parameters, used internally to give the plus parameter its number plusCount int // number of plus parameters, used internally to give the plus parameter its number
} }
var routerParserPool = &sync.Pool{
New: func() any {
return &routeParser{}
},
}
// routeSegment holds the segment metadata // routeSegment holds the segment metadata
type routeSegment struct { type routeSegment struct {
// const information // const information
@ -123,8 +131,6 @@ var (
parameterConstraintSeparatorChars = []byte{paramConstraintSeparator} parameterConstraintSeparatorChars = []byte{paramConstraintSeparator}
// list of parameter constraint data start // list of parameter constraint data start
parameterConstraintDataStartChars = []byte{paramConstraintDataStart} parameterConstraintDataStartChars = []byte{paramConstraintDataStart}
// list of parameter constraint data end
parameterConstraintDataEndChars = []byte{paramConstraintDataEnd}
// list of parameter constraint data separator // list of parameter constraint data separator
parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator} parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator}
) )
@ -152,11 +158,11 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
pattern = "/" + pattern pattern = "/" + pattern
} }
patternPretty := pattern patternPretty := []byte(pattern)
// Case-sensitive routing, all to lowercase // Case-sensitive routing, all to lowercase
if !config.CaseSensitive { if !config.CaseSensitive {
patternPretty = utils.ToLower(patternPretty) patternPretty = utils.ToLowerBytes(patternPretty)
path = utils.ToLower(path) path = utils.ToLower(path)
} }
// Strict routing, remove trailing slashes // Strict routing, remove trailing slashes
@ -164,12 +170,15 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
patternPretty = utils.TrimRight(patternPretty, '/') patternPretty = utils.TrimRight(patternPretty, '/')
} }
parser := parseRoute(patternPretty) parser, _ := routerParserPool.Get().(*routeParser) //nolint:errcheck // only contains routeParser
parser.reset()
parser.parseRoute(string(patternPretty))
defer routerParserPool.Put(parser)
if patternPretty == "/" && path == "/" { if string(patternPretty) == "/" && path == "/" {
return true return true
// '*' wildcard matches any path // '*' wildcard matches any path
} else if patternPretty == "/*" { } else if string(patternPretty) == "/*" {
return true return true
} }
@ -180,42 +189,47 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
} }
} }
// Check for a simple match // Check for a simple match
patternPretty = RemoveEscapeChar(patternPretty) patternPretty = RemoveEscapeCharBytes(patternPretty)
if len(patternPretty) == len(path) && patternPretty == path {
return true return string(patternPretty) == path
} }
// No match
return false func (parser *routeParser) reset() {
parser.segs = parser.segs[:0]
parser.params = parser.params[:0]
parser.wildCardCount = 0
parser.plusCount = 0
} }
// parseRoute analyzes the route and divides it into segments for constant areas and parameters, // parseRoute analyzes the route and divides it into segments for constant areas and parameters,
// this information is needed later when assigning the requests to the declared routes // this information is needed later when assigning the requests to the declared routes
func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { func (parser *routeParser) parseRoute(pattern string, customConstraints ...CustomConstraint) {
parser := routeParser{} var n int
part := "" var seg *routeSegment
for len(pattern) > 0 { for len(pattern) > 0 {
nextParamPosition := findNextParamPosition(pattern) nextParamPosition := findNextParamPosition(pattern)
// handle the parameter part // handle the parameter part
if nextParamPosition == 0 { if nextParamPosition == 0 {
processedPart, seg := parser.analyseParameterPart(pattern, customConstraints...) n, seg = parser.analyseParameterPart(pattern, customConstraints...)
parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart parser.params, parser.segs = append(parser.params, seg.ParamName), append(parser.segs, seg)
} else { } else {
processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition) n, seg = parser.analyseConstantPart(pattern, nextParamPosition)
parser.segs, part = append(parser.segs, seg), processedPart parser.segs = append(parser.segs, seg)
} }
pattern = pattern[n:]
// reduce the pattern by the processed parts
if len(part) == len(pattern) {
break
}
pattern = pattern[len(part):]
} }
// mark last segment // mark last segment
if len(parser.segs) > 0 { if len(parser.segs) > 0 {
parser.segs[len(parser.segs)-1].IsLast = true parser.segs[len(parser.segs)-1].IsLast = true
} }
parser.segs = addParameterMetaInfo(parser.segs) parser.segs = addParameterMetaInfo(parser.segs)
}
// parseRoute analyzes the route and divides it into segments for constant areas and parameters,
// this information is needed later when assigning the requests to the declared routes
func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser {
parser := routeParser{}
parser.parseRoute(pattern, customConstraints...)
return parser return parser
} }
@ -283,7 +297,7 @@ func findNextParamPosition(pattern string) int {
} }
// analyseConstantPart find the end of the constant part and create the route segment // analyseConstantPart find the end of the constant part and create the route segment
func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (int, *routeSegment) {
// handle the constant part // handle the constant part
processedPart := pattern processedPart := pattern
if nextParamPosition != -1 { if nextParamPosition != -1 {
@ -291,14 +305,14 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (
processedPart = pattern[:nextParamPosition] processedPart = pattern[:nextParamPosition]
} }
constPart := RemoveEscapeChar(processedPart) constPart := RemoveEscapeChar(processedPart)
return processedPart, &routeSegment{ return len(processedPart), &routeSegment{
Const: constPart, Const: constPart,
Length: len(constPart), Length: len(constPart),
} }
} }
// analyseParameterPart find the parameter end and create the route segment // analyseParameterPart find the parameter end and create the route segment
func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (string, *routeSegment) { func (parser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) {
isWildCard := pattern[0] == wildcardParam isWildCard := pattern[0] == wildcardParam
isPlusParam := pattern[0] == plusParam isPlusParam := pattern[0] == plusParam
@ -317,18 +331,19 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
parameterEndPosition = 0 parameterEndPosition = 0
case parameterEndPosition == -1: case parameterEndPosition == -1:
parameterEndPosition = len(pattern) - 1 parameterEndPosition = len(pattern) - 1
case !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars): case bytes.IndexByte(parameterDelimiterChars, pattern[parameterEndPosition+1]) == -1:
parameterEndPosition++ parameterEndPosition++
} }
// find constraint part if exists in the parameter part and remove it // find constraint part if exists in the parameter part and remove it
if parameterEndPosition > 0 { if parameterEndPosition > 0 {
parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars) parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars)
parameterConstraintEnd = findLastCharsetPosition(pattern[0:parameterEndPosition+1], parameterConstraintEndChars) parameterConstraintEnd = strings.LastIndexByte(pattern[0:parameterEndPosition+1], paramConstraintEnd)
} }
// cut params part // cut params part
processedPart := pattern[0 : parameterEndPosition+1] processedPart := pattern[0 : parameterEndPosition+1]
n := parameterEndPosition + 1
paramName := RemoveEscapeChar(GetTrimmedParam(processedPart)) paramName := RemoveEscapeChar(GetTrimmedParam(processedPart))
// Check has constraint // Check has constraint
@ -341,7 +356,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
for _, c := range userConstraints { for _, c := range userConstraints {
start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars) start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars)
end := findLastCharsetPosition(c, parameterConstraintDataEndChars) end := strings.LastIndexByte(c, paramConstraintDataEnd)
// Assign constraint // Assign constraint
if start != -1 && end != -1 { if start != -1 && end != -1 {
@ -384,11 +399,11 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
// add access iterator to wildcard and plus // add access iterator to wildcard and plus
if isWildCard { if isWildCard {
routeParser.wildCardCount++ parser.wildCardCount++
paramName += strconv.Itoa(routeParser.wildCardCount) paramName += strconv.Itoa(parser.wildCardCount)
} else if isPlusParam { } else if isPlusParam {
routeParser.plusCount++ parser.plusCount++
paramName += strconv.Itoa(routeParser.plusCount) paramName += strconv.Itoa(parser.plusCount)
} }
segment := &routeSegment{ segment := &routeSegment{
@ -402,17 +417,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
segment.Constraints = constraints segment.Constraints = constraints
} }
return processedPart, segment return n, segment
}
// isInCharset check is the given character in the charset list
func isInCharset(searchChar byte, charset []byte) bool {
for _, char := range charset {
if char == searchChar {
return true
}
}
return false
} }
// findNextCharsetPosition search the next char position from the charset // findNextCharsetPosition search the next char position from the charset
@ -427,18 +432,6 @@ func findNextCharsetPosition(search string, charset []byte) int {
return nextPosition return nextPosition
} }
// findLastCharsetPosition search the last char position from the charset
func findLastCharsetPosition(search string, charset []byte) int {
lastPosition := -1
for _, char := range charset {
if pos := strings.LastIndexByte(search, char); pos != -1 && (pos < lastPosition || lastPosition == -1) {
lastPosition = pos
}
}
return lastPosition
}
// findNextCharsetPositionConstraint search the next char position from the charset // findNextCharsetPositionConstraint search the next char position from the charset
// unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern // unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern
func findNextCharsetPositionConstraint(search string, charset []byte) int { func findNextCharsetPositionConstraint(search string, charset []byte) int {
@ -494,9 +487,9 @@ func splitNonEscaped(s, sep string) []string {
} }
// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here func (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint:revive // Accepting a bool param is fine here
var i, paramsIterator, partLen int var i, paramsIterator, partLen int
for _, segment := range routeParser.segs { for _, segment := range parser.segs {
partLen = len(detectionPath) partLen = len(detectionPath)
// check const segment // check const segment
if !segment.IsParam { if !segment.IsParam {
@ -618,7 +611,7 @@ func GetTrimmedParam(param string) string {
return param[start:end] return param[start:end]
} }
// RemoveEscapeChar remove escape characters // RemoveEscapeChar removes escape characters
func RemoveEscapeChar(word string) string { func RemoveEscapeChar(word string) string {
b := []byte(word) b := []byte(word)
dst := 0 dst := 0
@ -632,6 +625,18 @@ func RemoveEscapeChar(word string) string {
return string(b[:dst]) return string(b[:dst])
} }
// RemoveEscapeCharBytes removes escape characters
func RemoveEscapeCharBytes(word []byte) []byte {
dst := 0
for src := 0; src < len(word); src++ {
if word[src] != '\\' {
word[dst] = word[src]
dst++
}
}
return word[:dst]
}
func getParamConstraintType(constraintPart string) TypeConstraint { func getParamConstraintType(constraintPart string) TypeConstraint {
switch constraintPart { switch constraintPart {
case ConstraintInt: case ConstraintInt:

View File

@ -217,7 +217,7 @@ func Benchmark_Path_matchParams(t *testing.B) {
state = "not match" state = "not match"
} }
t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) {
for i := 0; i <= b.N; i++ { for i := 0; i < b.N; i++ {
if match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match { if match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match {
// Get testCases from the original path // Get testCases from the original path
matchRes = true matchRes = true
@ -250,7 +250,7 @@ func Benchmark_RoutePatternMatch(t *testing.B) {
state = "not match" state = "not match"
} }
t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) {
for i := 0; i <= b.N; i++ { for i := 0; i < b.N; i++ {
if match := RoutePatternMatch(c.url, testCollection.pattern); match { if match := RoutePatternMatch(c.url, testCollection.pattern); match {
// Get testCases from the original path // Get testCases from the original path
matchRes = true matchRes = true

View File

@ -178,7 +178,7 @@ func Test_Redirect_Back_WithFlashMessages(t *testing.T) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
err := c.Redirect().With("success", "1").With("message", "test").Back("/") err := c.Redirect().With("success", "1").With("message", "test").Back("/")
require.NoError(t, err) require.NoError(t, err)
@ -225,7 +225,7 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
err := c.Redirect().With("success", "1").With("message", "test").Route("user") err := c.Redirect().With("success", "1").With("message", "test").Route("user")
@ -259,7 +259,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().URI().SetQueryString("id=1&name=tom") c.Request().URI().SetQueryString("id=1&name=tom")
err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user")
@ -294,7 +294,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Request().Header.Set(HeaderContentType, MIMEApplicationForm) c.Request().Header.Set(HeaderContentType, MIMEApplicationForm)
c.Request().SetBodyString("id=1&name=tom") c.Request().SetBodyString("id=1&name=tom")
@ -330,7 +330,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
@ -376,7 +376,7 @@ func Test_Redirect_parseAndClearFlashMessages(t *testing.T) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
msgs := redirectionMsgs{ msgs := redirectionMsgs{
{ {
@ -464,7 +464,7 @@ func Benchmark_Redirect_Route(b *testing.B) {
return c.JSON(c.Params("name")) return c.JSON(c.Params("name"))
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -491,7 +491,7 @@ func Benchmark_Redirect_Route_WithQueries(b *testing.B) {
return c.JSON(c.Params("name")) return c.JSON(c.Params("name"))
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -523,7 +523,7 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ReportAllocs() b.ReportAllocs()
b.ResetTimer() b.ResetTimer()
@ -576,7 +576,7 @@ func Benchmark_Redirect_parseAndClearFlashMessages(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
val, err := testredirectionMsgs.MarshalMsg(nil) val, err := testredirectionMsgs.MarshalMsg(nil)
require.NoError(b, err) require.NoError(b, err)
@ -618,7 +618,7 @@ func Benchmark_Redirect_processFlashMessages(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
c.Redirect().With("success", "1").With("message", "test") c.Redirect().With("success", "1").With("message", "test")
@ -647,7 +647,7 @@ func Benchmark_Redirect_Messages(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
val, err := testredirectionMsgs.MarshalMsg(nil) val, err := testredirectionMsgs.MarshalMsg(nil)
require.NoError(b, err) require.NoError(b, err)
@ -684,7 +684,7 @@ func Benchmark_Redirect_OldInputs(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
val, err := testredirectionMsgs.MarshalMsg(nil) val, err := testredirectionMsgs.MarshalMsg(nil)
require.NoError(b, err) require.NoError(b, err)
@ -719,7 +719,7 @@ func Benchmark_Redirect_Message(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
val, err := testredirectionMsgs.MarshalMsg(nil) val, err := testredirectionMsgs.MarshalMsg(nil)
require.NoError(b, err) require.NoError(b, err)
@ -750,7 +750,7 @@ func Benchmark_Redirect_OldInput(b *testing.B) {
return c.SendString("user") return c.SendString("user")
}).Name("user") }).Name("user")
c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
val, err := testredirectionMsgs.MarshalMsg(nil) val, err := testredirectionMsgs.MarshalMsg(nil)
require.NoError(b, err) require.NoError(b, err)

159
req.go Normal file
View File

@ -0,0 +1,159 @@
package fiber
import (
"crypto/tls"
"mime/multipart"
)
//go:generate ifacemaker --file req.go --struct DefaultReq --iface Req --pkg fiber --output req_interface_gen.go --not-exported true --iface-comment "Req"
type DefaultReq struct {
ctx *DefaultCtx
}
func (r *DefaultReq) Accepts(offers ...string) string {
return r.ctx.Accepts(offers...)
}
func (r *DefaultReq) AcceptsCharsets(offers ...string) string {
return r.ctx.AcceptsCharsets(offers...)
}
func (r *DefaultReq) AcceptsEncodings(offers ...string) string {
return r.ctx.AcceptsEncodings(offers...)
}
func (r *DefaultReq) AcceptsLanguages(offers ...string) string {
return r.ctx.AcceptsLanguages(offers...)
}
func (r *DefaultReq) BaseURL() string {
return r.ctx.BaseURL()
}
func (r *DefaultReq) Body() []byte {
return r.ctx.Body()
}
func (r *DefaultReq) BodyRaw() []byte {
return r.ctx.BodyRaw()
}
func (r *DefaultReq) ClientHelloInfo() *tls.ClientHelloInfo {
return r.ctx.ClientHelloInfo()
}
func (r *DefaultReq) Cookies(key string, defaultValue ...string) string {
return r.ctx.Cookies(key, defaultValue...)
}
func (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) {
return r.ctx.FormFile(key)
}
func (r *DefaultReq) FormValue(key string, defaultValue ...string) string {
return r.ctx.FormValue(key, defaultValue...)
}
func (r *DefaultReq) Fresh() bool {
return r.ctx.Fresh()
}
func (r *DefaultReq) Get(key string, defaultValue ...string) string {
return r.ctx.Get(key, defaultValue...)
}
func (r *DefaultReq) Host() string {
return r.ctx.Host()
}
func (r *DefaultReq) Hostname() string {
return r.ctx.Hostname()
}
func (r *DefaultReq) IP() string {
return r.ctx.IP()
}
func (r *DefaultReq) IPs() []string {
return r.ctx.IPs()
}
func (r *DefaultReq) Is(extension string) bool {
return r.ctx.Is(extension)
}
func (r *DefaultReq) IsFromLocal() bool {
return r.ctx.IsFromLocal()
}
func (r *DefaultReq) IsProxyTrusted() bool {
return r.ctx.IsProxyTrusted()
}
func (r *DefaultReq) Method(override ...string) string {
return r.ctx.Method(override...)
}
func (r *DefaultReq) MultipartForm() (*multipart.Form, error) {
return r.ctx.MultipartForm()
}
func (r *DefaultReq) OriginalURL() string {
return r.ctx.OriginalURL()
}
func (r *DefaultReq) Params(key string, defaultValue ...string) string {
return r.ctx.Params(key, defaultValue...)
}
func (r *DefaultReq) Path(override ...string) string {
return r.ctx.Path(override...)
}
func (r *DefaultReq) Port() string {
return r.ctx.Port()
}
func (r *DefaultReq) Protocol() string {
return r.ctx.Protocol()
}
func (r *DefaultReq) Queries() map[string]string {
return r.ctx.Queries()
}
func (r *DefaultReq) Query(key string, defaultValue ...string) string {
return r.ctx.Query(key, defaultValue...)
}
func (r *DefaultReq) Range(size int) (Range, error) {
return r.ctx.Range(size)
}
func (r *DefaultReq) Route() *Route {
return r.ctx.Route()
}
func (r *DefaultReq) SaveFile(fileheader *multipart.FileHeader, path string) error {
return r.ctx.SaveFile(fileheader, path)
}
func (r *DefaultReq) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error {
return r.ctx.SaveFileToStorage(fileheader, path, storage)
}
func (r *DefaultReq) Secure() bool {
return r.ctx.Secure()
}
func (r *DefaultReq) Stale() bool {
return r.ctx.Stale()
}
func (r *DefaultReq) Subdomains(offset ...int) []string {
return r.ctx.Subdomains(offset...)
}
func (r *DefaultReq) XHR() bool {
return r.ctx.XHR()
}

49
req_interface_gen.go Normal file
View File

@ -0,0 +1,49 @@
// Code generated by ifacemaker; DO NOT EDIT.
package fiber
import (
"crypto/tls"
"mime/multipart"
)
// Req
type Req interface {
Accepts(offers ...string) string
AcceptsCharsets(offers ...string) string
AcceptsEncodings(offers ...string) string
AcceptsLanguages(offers ...string) string
BaseURL() string
Body() []byte
BodyRaw() []byte
ClientHelloInfo() *tls.ClientHelloInfo
Cookies(key string, defaultValue ...string) string
FormFile(key string) (*multipart.FileHeader, error)
FormValue(key string, defaultValue ...string) string
Fresh() bool
Get(key string, defaultValue ...string) string
Host() string
Hostname() string
IP() string
IPs() []string
Is(extension string) bool
IsFromLocal() bool
IsProxyTrusted() bool
Method(override ...string) string
MultipartForm() (*multipart.Form, error)
OriginalURL() string
Params(key string, defaultValue ...string) string
Path(override ...string) string
Port() string
Protocol() string
Queries() map[string]string
Query(key string, defaultValue ...string) string
Range(size int) (Range, error)
Route() *Route
SaveFile(fileheader *multipart.FileHeader, path string) error
SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error
Secure() bool
Stale() bool
Subdomains(offset ...int) []string
XHR() bool
}

118
res.go Normal file
View File

@ -0,0 +1,118 @@
package fiber
import (
"bufio"
)
//go:generate ifacemaker --file res.go --struct DefaultRes --iface Res --pkg fiber --output res_interface_gen.go --not-exported true --iface-comment "Res"
type DefaultRes struct {
ctx *DefaultCtx
}
func (r *DefaultRes) Append(field string, values ...string) {
r.ctx.Append(field, values...)
}
func (r *DefaultRes) Attachment(filename ...string) {
r.ctx.Attachment(filename...)
}
func (r *DefaultRes) AutoFormat(body any) error {
return r.ctx.AutoFormat(body)
}
func (r *DefaultRes) CBOR(body any, ctype ...string) error {
return r.ctx.CBOR(body, ctype...)
}
func (r *DefaultRes) ClearCookie(key ...string) {
r.ctx.ClearCookie(key...)
}
func (r *DefaultRes) Cookie(cookie *Cookie) {
r.ctx.Cookie(cookie)
}
func (r *DefaultRes) Download(file string, filename ...string) error {
return r.ctx.Download(file, filename...)
}
func (r *DefaultRes) Format(handlers ...ResFmt) error {
return r.ctx.Format(handlers...)
}
func (r *DefaultRes) Get(key string, defaultValue ...string) string {
return r.ctx.GetRespHeader(key, defaultValue...)
}
func (r *DefaultRes) JSON(body any, ctype ...string) error {
return r.ctx.JSON(body, ctype...)
}
func (r *DefaultRes) JSONP(data any, callback ...string) error {
return r.ctx.JSONP(data, callback...)
}
func (r *DefaultRes) Links(link ...string) {
r.ctx.Links(link...)
}
func (r *DefaultRes) Location(path string) {
r.ctx.Location(path)
}
func (r *DefaultRes) Render(name string, bind any, layouts ...string) error {
return r.ctx.Render(name, bind, layouts...)
}
func (r *DefaultRes) Send(body []byte) error {
return r.ctx.Send(body)
}
func (r *DefaultRes) SendFile(file string, config ...SendFile) error {
return r.ctx.SendFile(file, config...)
}
func (r *DefaultRes) SendStatus(status int) error {
return r.ctx.SendStatus(status)
}
func (r *DefaultRes) SendString(body string) error {
return r.ctx.SendString(body)
}
func (r *DefaultRes) SendStreamWriter(streamWriter func(*bufio.Writer)) error {
return r.ctx.SendStreamWriter(streamWriter)
}
func (r *DefaultRes) Set(key, val string) {
r.ctx.Set(key, val)
}
func (r *DefaultRes) Status(status int) Ctx {
return r.ctx.Status(status)
}
func (r *DefaultRes) Type(extension string, charset ...string) Ctx {
return r.ctx.Type(extension, charset...)
}
func (r *DefaultRes) Vary(fields ...string) {
r.ctx.Vary(fields...)
}
func (r *DefaultRes) Write(p []byte) (int, error) {
return r.ctx.Write(p)
}
func (r *DefaultRes) Writef(f string, a ...any) (int, error) {
return r.ctx.Writef(f, a...)
}
func (r *DefaultRes) WriteString(s string) (int, error) {
return r.ctx.WriteString(s)
}
func (r *DefaultRes) XML(data any) error {
return r.ctx.XML(data)
}

38
res_interface_gen.go Normal file
View File

@ -0,0 +1,38 @@
// Code generated by ifacemaker; DO NOT EDIT.
package fiber
import (
"bufio"
)
// Res
type Res interface {
Append(field string, values ...string)
Attachment(filename ...string)
AutoFormat(body any) error
CBOR(body any, ctype ...string) error
ClearCookie(key ...string)
Cookie(cookie *Cookie)
Download(file string, filename ...string) error
Format(handlers ...ResFmt) error
Get(key string, defaultValue ...string) string
JSON(body any, ctype ...string) error
JSONP(data any, callback ...string) error
Links(link ...string)
Location(path string)
Render(name string, bind any, layouts ...string) error
Send(body []byte) error
SendFile(file string, config ...SendFile) error
SendStatus(status int) error
SendString(body string) error
SendStreamWriter(streamWriter func(*bufio.Writer)) error
Set(key, val string)
Status(status int) Ctx
Type(extension string, charset ...string) Ctx
Vary(fields ...string)
Write(p []byte) (int, error)
Writef(f string, a ...any) (int, error)
WriteString(s string) (int, error)
XML(data any) error
}

View File

@ -106,11 +106,11 @@ func (r *Route[TCtx]) match(detectionPath, path string, params *[maxParams]strin
return false return false
} }
func (app *App[TCtx]) next(c TCtx) (bool, error) { //nolint: unparam // bool param might be useful for testing func (app *App[TCtx]) next(c TCtx) (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
@ -154,6 +154,61 @@ func (app *App[TCtx]) next(c TCtx) (bool, error) { //nolint: unparam // bool par
return false, err return false, err
} }
func (app *App) next2222(c *DefaultCtx) (bool, error) {
// Get stack length
tree, ok := app.treeStack[c.methodInt][c.treePathHash]
if !ok {
tree = app.treeStack[c.methodInt][0]
}
lenTree := len(tree) - 1
// Loop over the route stack starting from previous index
for c.indexRoute < lenTree {
// Increment route index
c.indexRoute++
// Get *Route
route := tree[c.indexRoute]
var match bool
var err error
// skip for mounted apps
if route.mount {
continue
}
// Check if it matches the request path
match = route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values)
if !match {
// No match, next route
continue
}
// Pass route reference and param values
c.route = route
// Non use handler matched
if !c.matched && !route.use {
c.matched = true
}
// Execute first handler of route
c.indexHandler = 0
if len(route.Handlers) > 0 {
err = route.Handlers[0](c)
}
return match, err // Stop scanning the stack
}
// If c.Next() does not match, return 404
err := NewError(StatusNotFound, "Cannot "+c.Method()+" "+html.EscapeString(c.pathOriginal))
if !c.matched && app.methodExist(c) {
// If no match, scan stack again if other methods match the request
// Moved from app.handler because middleware may break the route chain
err = ErrMethodNotAllowed
}
return false, err
}
func (app *App[TCtx]) requestHandler(rctx *fasthttp.RequestCtx) { func (app *App[TCtx]) requestHandler(rctx *fasthttp.RequestCtx) {
// Acquire DefaultCtx from the pool // Acquire DefaultCtx from the pool
ctx := app.AcquireCtx(rctx) ctx := app.AcquireCtx(rctx)
@ -161,7 +216,7 @@ func (app *App[TCtx]) requestHandler(rctx *fasthttp.RequestCtx) {
defer app.ReleaseCtx(ctx) defer app.ReleaseCtx(ctx)
// Check if the HTTP method is valid // Check if the HTTP method is valid
if app.methodInt(ctx.Method()) == -1 { if ctx.methodInt == -1 {
_ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil _ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
return return
} }
@ -363,30 +418,28 @@ func (app *App[TCtx]) buildTree() *App[TCtx] {
// 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[TCtx]) tsMap := make(map[int][]*Route[TCtx])
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[TCtx](append(tsMap[treePart], tsMap[""]...)) tsMap[treePart] = uniqueRouteStack[TCtx](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

View File

@ -600,7 +600,7 @@ func Benchmark_Router_Next(b *testing.B) {
var res bool var res bool
var err error var err error
c := app.AcquireCtx(request).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed c := app.AcquireCtx(request).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
b.ResetTimer() b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
@ -656,6 +656,50 @@ func Benchmark_Router_Next_Default_Parallel(b *testing.B) {
}) })
} }
// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default_Immutable -benchmem -count=4
func Benchmark_Router_Next_Default_Immutable(b *testing.B) {
app := New(Config{Immutable: true})
app.Get("/", func(_ Ctx) error {
return nil
})
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(MethodGet)
fctx.Request.SetRequestURI("/")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
h(fctx)
}
}
// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel_Immutable$ github.com/gofiber/fiber/v3 -count=1
func Benchmark_Router_Next_Default_Parallel_Immutable(b *testing.B) {
app := New(Config{Immutable: true})
app.Get("/", func(_ Ctx) error {
return nil
})
h := app.Handler()
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(MethodGet)
fctx.Request.SetRequestURI("/")
for pb.Next() {
h(fctx)
}
})
}
// go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4 // go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4
func Benchmark_Route_Match(b *testing.B) { func Benchmark_Route_Match(b *testing.B) {
var match bool var match bool
@ -825,7 +869,7 @@ func Benchmark_Router_Github_API(b *testing.B) {
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
c.URI().SetPath(routesFixture.TestRoutes[i].Path) c.URI().SetPath(routesFixture.TestRoutes[i].Path)
ctx := app.AcquireCtx(c).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed ctx := app.AcquireCtx(c).(*DefaultCtx) //nolint:errcheck,forcetypeassert // not needed
match, err = app.next(ctx) match, err = app.next(ctx)
app.ReleaseCtx(ctx) app.ReleaseCtx(ctx)