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

1
.github/release.yml vendored
View File

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

View File

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

View File

@ -37,4 +37,7 @@ jobs:
uses: golangci/golangci-lint-action@v6
with:
# 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
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:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.txt

View File

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

View File

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

View File

@ -19,17 +19,47 @@ a jitter is a way to break synchronization across the client and avoid collision
## Signatures
```go
func NewExponentialBackoff(config ...Config) *ExponentialBackoff
func NewExponentialBackoff(config ...retry.Config) *retry.ExponentialBackoff
```
## Examples
Firstly, import the addon from Fiber,
```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
@ -58,28 +88,23 @@ type Config struct {
//
// 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
// 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
stack [][]*Route[TCtx]
// Route stack divided by HTTP methods and route prefixes
treeStack []map[string][]*Route[TCtx]
treeStack []map[int][]*Route[TCtx]
// custom binders
customBinders []CustomBinder
// customConstraints is a list of external constraints
@ -456,17 +456,29 @@ const (
DefaultWriteBufferSize = 4096
)
const (
methodGet = iota
methodHead
methodPost
methodPut
methodDelete
methodConnect
methodOptions
methodTrace
methodPatch
)
// HTTP methods enabled by default
var DefaultMethods = []string{
MethodGet,
MethodHead,
MethodPost,
MethodPut,
MethodDelete,
MethodConnect,
MethodOptions,
MethodTrace,
MethodPatch,
methodGet: MethodGet,
methodHead: MethodHead,
methodPost: MethodPost,
methodPut: MethodPut,
methodDelete: MethodDelete,
methodConnect: MethodConnect,
methodOptions: MethodOptions,
methodTrace: MethodTrace,
methodPatch: MethodPatch,
}
// DefaultErrorHandler that process return errors from handlers
@ -621,7 +633,7 @@ func newApp[TCtx CtxGeneric[TCtx]](config ...Config[TCtx]) *App[TCtx] {
// Create router stack
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
app.config.ColorScheme = defaultColors(app.config.ColorScheme)
@ -1048,7 +1060,7 @@ func (app *App[TCtx]) Test(req *http.Request, config ...TestConfig) (*http.Respo
select {
case err = <-channel:
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 {
return nil, os.ErrDeadlineExceeded
}

View File

@ -403,7 +403,7 @@ func Test_App_serverErrorHandler_Internal_Error(t *testing.T) {
t.Parallel()
app := New()
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))
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) {
t.Parallel()
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{
Err: "test error",

View File

@ -1378,7 +1378,7 @@ func Benchmark_Bind_URI(b *testing.B) {
var err error
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{
Params: []string{
@ -1415,7 +1415,7 @@ func Benchmark_Bind_URI_Map(b *testing.B) {
var err error
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{
Params: []string{

View File

@ -28,7 +28,7 @@ Fiber provides several default binders out of the box:
### 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
// Field names must start with an uppercase letter

View File

@ -58,19 +58,19 @@ func Benchmark_FormBinder_Bind(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
binder := &QueryBinding{
binder := &FormBinding{
EnableSplitting: true,
}
type User struct {
Name string `query:"name"`
Posts []string `query:"posts"`
Age int `query:"age"`
Name string `form:"name"`
Posts []string `form:"posts"`
Age int `form:"age"`
}
var user User
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")
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...)
}
// 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 {
// Get decoder from pool
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()
app, ln, start := createHelperServer(t)

View File

@ -199,7 +199,7 @@ func parserRequestBody(c *Client, req *Request) error {
case filesBody:
return parserRequestBodyFile(req)
case rawBody:
if body, ok := req.body.([]byte); ok {
if body, ok := req.body.([]byte); ok { //nolint:revive // ignore simplicity
req.RawRequest.SetBody(body)
} else {
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.
func (r *Request) Cookies() iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
r.cookies.VisitAll(func(key, val string) {
if !yield(key, val) {
for k, v := range *r.cookies {
res := yield(k, v)
if !res {
return
}
})
}
}
}
@ -343,11 +344,11 @@ func (r *Request) PathParam(key string) string {
// Use maps.Collect() to gather them into a map if needed.
func (r *Request) PathParams() iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
r.path.VisitAll(func(key, val string) {
if !yield(key, val) {
for k, v := range *r.path {
if !yield(k, v) {
return
}
})
}
}
}

View File

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

115
ctx.go
View File

@ -33,8 +33,11 @@ const (
schemeHTTPS = "https"
)
// maxParams defines the maximum number of parameters per route.
const maxParams = 30
const (
// maxParams defines the maximum number of parameters per route.
maxParams = 30
maxDetectionPaths = 3
)
// The contextKey type is unexported to prevent collisions with context keys defined in
// other packages.
@ -50,26 +53,25 @@ const userContextKey contextKey = 0 // __local_user_context__
//go:generate ifacemaker --file ctx.go --struct DefaultCtx --iface CtxGeneric --pkg fiber --output ctx_interface.go --not-exported true --iface-comment "Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on."
//go:generate go run ctx_interface_gen.go
type DefaultCtx struct {
app *App[*DefaultCtx] // Reference to *App
route *Route[*DefaultCtx] // Reference to *Route
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
bind *Bind // Default bind reference
redirect *Redirect // Default redirect reference
values [maxParams]string // Route parameter values
viewBindMap sync.Map // Default view map to bind template engine
method string // HTTP method
baseURI string // HTTP base uri
path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer
detectionPath string // Route detection path -> string copy from detectionPathBuffer
treePath string // Path for the search in the tree
pathOriginal string // Original HTTP path
pathBuffer []byte // HTTP path buffer
detectionPathBuffer []byte // HTTP detectionPath buffer
flashMessages redirectionMsgs // Flash messages
indexRoute int // Index of the current route
indexHandler int // Index of the current handler
methodINT int // HTTP method INT equivalent
matched bool // Non use route matched
app *App[*DefaultCtx] // Reference to *App
route *Route[*DefaultCtx] // Reference to *Route
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
bind *Bind // Default bind reference
redirect *Redirect // Default redirect reference
req *DefaultReq // Default request api reference
res *DefaultRes // Default response api reference
values [maxParams]string // Route parameter values
viewBindMap sync.Map // Default view map to bind template engine
baseURI string // HTTP base uri
pathOriginal string // Original HTTP path
flashMessages redirectionMsgs // Flash messages
path []byte // HTTP path with the modifications by the configuration
detectionPath []byte // Route detection path
treePathHash int // Hash of the path for the search in the tree
indexRoute int // Index of the current route
indexHandler int // Index of the current handler
methodInt int // HTTP method INT equivalent
matched bool // Non use route matched
}
type Ctx = CtxGeneric[*DefaultCtx]
@ -1006,19 +1008,17 @@ func (c *DefaultCtx) Location(path string) {
func (c *DefaultCtx) Method(override ...string) string {
if len(override) == 0 {
// Nothing to override, just return current method from context
return c.method
return c.app.method(c.methodInt)
}
method := utils.ToUpper(override[0])
mINT := c.app.methodInt(method)
if mINT == -1 {
methodInt := c.app.methodInt(method)
if methodInt == -1 {
// Provided override does not valid HTTP method, no override, return current method
return c.method
return c.app.method(c.methodInt)
}
c.method = method
c.methodINT = mINT
return c.method
c.methodInt = methodInt
return method
}
// 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.
// Optionally, you could override the path.
// Make copies or use the Immutable setting to use the value outside the Handler.
func (c *DefaultCtx) Path(override ...string) string {
if len(override) != 0 && c.path != override[0] {
if len(override) != 0 && string(c.path) != override[0] {
// Set new path to context
c.pathOriginal = override[0]
@ -1136,7 +1137,7 @@ func (c *DefaultCtx) Path(override ...string) string {
// Prettify path
c.configDependentPaths()
}
return c.path
return c.app.getString(c.path)
}
// Scheme contains the request protocol string: http or https for TLS requests.
@ -1352,7 +1353,7 @@ func (c *DefaultCtx) getLocationFromRoute(route Route, params Map) (string, erro
for key, val := range params {
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 {
_, err := buf.WriteString(utils.ToString(val))
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.
func (c *DefaultCtx) Route() *Route {
if c.route == nil {
@ -1475,7 +1488,7 @@ func (c *DefaultCtx) Route() *Route {
return &Route{
path: c.pathOriginal,
Path: c.pathOriginal,
Method: c.method,
Method: c.Method(),
Handlers: make([]Handler, 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,
// here the features for caseSensitive, decoded paths, strict paths are evaluated
func (c *DefaultCtx) configDependentPaths() {
c.pathBuffer = append(c.pathBuffer[0:0], c.pathOriginal...)
c.path = append(c.path[:0], c.pathOriginal...)
// If UnescapePath enabled, we decode the path and save it for the framework user
if c.app.config.UnescapePath {
c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer)
c.path = fasthttp.AppendUnquotedArg(c.path[:0], c.path)
}
c.path = c.app.getString(c.pathBuffer)
// another path is specified which is for routing recognition only
// use the path that was changed by the previous configuration flags
c.detectionPathBuffer = append(c.detectionPathBuffer[0:0], c.pathBuffer...)
c.detectionPath = append(c.detectionPath[:0], c.path...)
// If CaseSensitive is disabled, we lowercase the original path
if !c.app.config.CaseSensitive {
c.detectionPathBuffer = utils.ToLowerBytes(c.detectionPathBuffer)
c.detectionPath = utils.ToLowerBytes(c.detectionPath)
}
// If StrictRouting is disabled, we strip all trailing slashes
if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' {
c.detectionPathBuffer = utils.TrimRight(c.detectionPathBuffer, '/')
if !c.app.config.StrictRouting && len(c.detectionPath) > 1 && c.detectionPath[len(c.detectionPath)-1] == '/' {
c.detectionPath = utils.TrimRight(c.detectionPath, '/')
}
c.detectionPath = c.app.getString(c.detectionPathBuffer)
// Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed,
// since the first three characters area select a list of routes
c.treePath = c.treePath[0:0]
const maxDetectionPaths = 3
c.treePathHash = 0
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
c.pathOriginal = c.app.getString(fctx.URI().PathOriginal())
// Set method
c.method = c.app.getString(fctx.Request.Header.Method())
c.methodINT = c.app.methodInt(c.method)
c.methodInt = c.app.methodInt(utils.UnsafeString(fctx.Request.Header.Method()))
// Attach *fasthttp.RequestCtx to ctx
c.fasthttp = fctx
// reset base uri
@ -1941,20 +1952,20 @@ func (c *DefaultCtx) getBody() []byte {
}
// Methods to use with next stack.
func (c *DefaultCtx) getMethodINT() int {
return c.methodINT
func (c *DefaultCtx) getMethodInt() int {
return c.methodInt
}
func (c *DefaultCtx) getIndexRoute() int {
return c.indexRoute
}
func (c *DefaultCtx) getTreePath() string {
return c.treePath
func (c *DefaultCtx) getTreePathHash() int {
return c.treePathHash
}
func (c *DefaultCtx) getDetectionPath() string {
return c.detectionPath
return c.app.getString(c.detectionPath)
}
func (c *DefaultCtx) getPathOriginal() string {

View File

@ -339,9 +339,9 @@ type CtxGeneric[T any] interface {
release()
getBody() []byte
// Methods to use with next stack.
getMethodINT() int
getMethodInt() int
getIndexRoute() int
getTreePath() string
getTreePathHash() int
getDetectionPath() string
getPathOriginal() string
getValues() *[maxParams]string
@ -349,11 +349,47 @@ type CtxGeneric[T any] interface {
setIndexHandler(handler int)
setIndexRoute(route int)
setMatched(matched bool)
setRoute(route *Route[T])
// 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.
Drop() error
// End immediately flushes the current response and closes the underlying connection.
End() error
setRoute(route *Route)
}
func NewDefaultCtx(app *App) *DefaultCtx {
// return ctx
ctx := &DefaultCtx{
// 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")
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, "", 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")
@ -57,13 +57,13 @@ func Test_Ctx_Accepts(t *testing.T) {
c.Request().Header.Set(HeaderAccept, "text/*, application/json")
require.Equal(t, "html", c.Accepts("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, "", c.Accepts("image/png"))
require.Equal(t, "", c.Accepts("png"))
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, "*/*")
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
func Benchmark_Ctx_AcceptsCharsets(b *testing.B) {
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")
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
func Benchmark_Ctx_AcceptsEncodings(b *testing.B) {
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")
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
func Benchmark_Ctx_AcceptsLanguages(b *testing.B) {
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")
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
func Benchmark_Ctx_Append(b *testing.B) {
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.ResetTimer()
@ -337,7 +337,7 @@ func Test_Ctx_Attachment(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4
func Benchmark_Ctx_Attachment(b *testing.B) {
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.ResetTimer()
@ -363,7 +363,7 @@ func Test_Ctx_BaseURL(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem
func Benchmark_Ctx_BaseURL(b *testing.B) {
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().URI().SetPath("/haha/oke/lol")
@ -380,7 +380,7 @@ func Benchmark_Ctx_BaseURL(b *testing.B) {
func Test_Ctx_Body(t *testing.T) {
t.Parallel()
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"))
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) {
t.Parallel()
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"))
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) {
t.Parallel()
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"))
require.Equal(t, []byte("john=doe"), c.BodyRaw())
@ -411,7 +411,7 @@ func Benchmark_Ctx_Body(b *testing.B) {
const input = "john=doe"
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))
b.ReportAllocs()
@ -428,7 +428,7 @@ func Benchmark_Ctx_BodyRaw(b *testing.B) {
const input = "john=doe"
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))
b.ReportAllocs()
@ -445,7 +445,7 @@ func Benchmark_Ctx_BodyRaw_Immutable(b *testing.B) {
const input = "john=doe"
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))
b.ReportAllocs()
@ -462,7 +462,7 @@ func Test_Ctx_Body_Immutable(t *testing.T) {
t.Parallel()
app := New()
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"))
require.Equal(t, []byte("john=doe"), c.Body())
@ -474,7 +474,7 @@ func Benchmark_Ctx_Body_Immutable(b *testing.B) {
app := New()
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))
b.ReportAllocs()
@ -527,7 +527,7 @@ func Test_Ctx_Body_With_Compression(t *testing.T) {
t.Run(tCase.name, func(t *testing.T) {
t.Parallel()
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)
if strings.Contains(tCase.contentEncoding, "gzip") {
@ -720,7 +720,7 @@ func Test_Ctx_Body_With_Compression_Immutable(t *testing.T) {
t.Parallel()
app := New()
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)
if strings.Contains(tCase.contentEncoding, "gzip") {
@ -897,7 +897,7 @@ func Test_Ctx_Context(t *testing.T) {
t.Parallel()
testKey := struct{}{}
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))
})
}
@ -910,7 +910,7 @@ func Test_Ctx_SetContext(t *testing.T) {
testKey := struct{}{}
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)
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"))
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)
return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input))
@ -968,52 +968,52 @@ func Test_Ctx_Cookie(t *testing.T) {
Expires: expire,
// SameSite: CookieSameSiteStrictMode, // default is "lax"
}
c.Cookie(cookie)
c.Res().Cookie(cookie)
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=/"
cookie.SameSite = CookieSameSiteDisabled
c.Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie)))
c.Res().Cookie(cookie)
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict"
cookie.SameSite = CookieSameSiteStrictMode
c.Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie)))
c.Res().Cookie(cookie)
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None"
cookie.Secure = true
cookie.SameSite = CookieSameSiteNoneMode
c.Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie)))
c.Res().Cookie(cookie)
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; path=/; secure; SameSite=None"
// should remove expires and max-age headers
cookie.SessionOnly = true
cookie.Expires = expire
cookie.MaxAge = 10000
c.Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie)))
c.Res().Cookie(cookie)
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; path=/; secure; SameSite=None"
// should remove expires and max-age headers when no expire and no MaxAge (default time)
cookie.SessionOnly = false
cookie.Expires = time.Time{}
cookie.MaxAge = 0
c.Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie)))
c.Res().Cookie(cookie)
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
expect = "username=john; path=/; secure; SameSite=None; Partitioned"
cookie.Partitioned = true
c.Cookie(cookie)
require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie)))
c.Res().Cookie(cookie)
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
}
// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4
func Benchmark_Ctx_Cookie(b *testing.B) {
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.ResetTimer()
@ -1033,8 +1033,8 @@ func Test_Ctx_Cookies(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.Set("Cookie", "john=doe")
require.Equal(t, "doe", c.Cookies("john"))
require.Equal(t, "default", c.Cookies("unknown", "default"))
require.Equal(t, "doe", c.Req().Cookies("john"))
require.Equal(t, "default", c.Req().Cookies("unknown", "default"))
}
// 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`)
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", c.GetRespHeader(HeaderContentType))
require.NoError(t, err)
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", c.GetRespHeader(HeaderContentType))
require.NoError(t, err)
@ -1165,7 +1165,7 @@ func Test_Ctx_AutoFormat(t *testing.T) {
require.Equal(t, "Hello, World!", string(c.Response().Body()))
c.Request().Header.Set(HeaderAccept, MIMETextHTML)
err = c.AutoFormat("Hello, World!")
err = c.Res().AutoFormat("Hello, World!")
require.NoError(t, err)
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()))
c.Request().Header.Set(HeaderAccept, MIMETextPlain)
err = c.AutoFormat(complex(1, 1))
err = c.Res().AutoFormat(complex(1, 1))
require.NoError(t, err)
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.Skip("URI is not ready for v3")
//nolint:gocritic // TODO: uncomment
//t.Parallel()
//withValues(t, func(c Ctx, testStruct *TestStruct) error {
// t.Parallel()
// withValues(t, func(c Ctx, testStruct *TestStruct) error {
// c.Route().Params = []string{"name", "name2", "class", "class2"}
// c.Params().value = [30]string{"foo", "bar", "111", "222"}
// return c.Bind().URI(testStruct)
//})
// })
})
t.Run("ReqHeader", func(t *testing.T) {
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
func Benchmark_Ctx_Params(b *testing.B) {
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{
Params: []string{
@ -2939,7 +2939,7 @@ func Test_Ctx_SaveFile(t *testing.T) {
app := New()
app.Post("/test", func(c Ctx) error {
fh, err := c.FormFile("file")
fh, err := c.Req().FormFile("file")
require.NoError(t, err)
tempFile, err := os.CreateTemp(os.TempDir(), "test-")
@ -3075,7 +3075,7 @@ func Test_Ctx_ClearCookie(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
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="))
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, `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)))
}
@ -3136,7 +3136,7 @@ func Test_Ctx_SendFile(t *testing.T) {
// test with custom error code
c = app.AcquireCtx(&fasthttp.RequestCtx{})
err = c.Status(StatusInternalServerError).SendFile("ctx.go")
err = c.Res().Status(StatusInternalServerError).SendFile("ctx.go")
// check expectation
require.NoError(t, err)
require.Equal(t, expectFileContent, c.Response().Body())
@ -3161,7 +3161,7 @@ func Test_Ctx_SendFile_ContentType(t *testing.T) {
// 1) simple case
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
require.NoError(t, err)
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) {
app := New()
// 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 {
Name string
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, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type")))
err = c.JSONP(Map{
err = c.Res().JSONP(Map{
"Name": "Grame",
"Age": 20,
}, "john")
@ -3813,7 +3813,7 @@ func Test_Ctx_JSONP(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4
func Benchmark_Ctx_JSONP(b *testing.B) {
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 {
Name string
@ -3838,7 +3838,7 @@ func Benchmark_Ctx_JSONP(b *testing.B) {
func Test_Ctx_XML(t *testing.T) {
t.Parallel()
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)))
@ -3897,7 +3897,7 @@ func Test_Ctx_XML(t *testing.T) {
// go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4
func Benchmark_Ctx_XML(b *testing.B) {
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 {
Name string `xml:"Name"`
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
func Benchmark_Ctx_Links(b *testing.B) {
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.ResetTimer()
@ -4006,7 +4006,7 @@ func Test_Ctx_Render(t *testing.T) {
err = c.Render("./.github/testdata/template-non-exists.html", nil)
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)
}
@ -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
func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) {
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 {
return c.SendString(c.Params("name"))
@ -4578,14 +4578,14 @@ func Test_Ctx_SendStreamWriter(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
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.Equal(t, "Don't crash please", string(c.Response().Body()))
err = c.SendStreamWriter(func(w *bufio.Writer) {
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 {
t.Errorf("unexpected error: %s", err)
return
@ -4607,7 +4607,7 @@ func Test_Ctx_SendStreamWriter_Interrupted(t *testing.T) {
app.Get("/", func(c Ctx) error {
return c.SendStreamWriter(func(w *bufio.Writer) {
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 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
func Benchmark_Ctx_Type_Charset(b *testing.B) {
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.ResetTimer()
@ -4753,7 +4753,7 @@ func Test_Ctx_Vary(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4
func Benchmark_Ctx_Vary(b *testing.B) {
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.ResetTimer()
@ -4806,7 +4806,7 @@ func Test_Ctx_Writef(t *testing.T) {
// go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4
func Benchmark_Ctx_Writef(b *testing.B) {
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!"
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")
queries = c.Queries()
queries = c.Req().Queries()
require.Equal(t, "apple,orange,banana", queries["tags"])
require.Equal(t, "apple,orange,banana", queries["filters[tags]"])
require.Equal(t, "fruits", queries["filters[category][name]"])
@ -4951,11 +4951,11 @@ func Test_Ctx_BodyStreamWriter(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
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 {
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())
@ -5055,7 +5055,7 @@ func Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
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.SetRemoteAddr(localIPv6)
c := app.AcquireCtx(fastCtx)
require.Equal(t, "::1", c.IP())
require.True(t, c.IsFromLocal())
require.Equal(t, "::1", c.Req().IP())
require.True(t, c.Req().IsFromLocal())
}
// 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",
"position": 5,
"position": 6,
"link": {
"type": "generated-index",
"description": "HTTP client for Fiber."

View File

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

View File

@ -1,6 +1,6 @@
{
"label": "\uD83D\uDCD6 Guide",
"position": 5,
"position": 7,
"link": {
"type": "generated-index",
"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.
```go title="Example"
```go title="Basic Example"
import "github.com/go-playground/validator/v10"
type structValidator struct {
@ -42,3 +41,71 @@ app.Post("/", func(c fiber.Ctx) error {
return c.JSON(user)
})
```
```go title="Advanced Validation Example"
type User struct {
Name string `json:"name" validate:"required,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=100"`
Password string `json:"password" validate:"required,min=8"`
Website string `json:"website" validate:"url"`
}
// Custom validation error messages
type UserWithCustomMessages struct {
Name string `json:"name" validate:"required,min=3,max=32" message:"Name is required and must be between 3 and 32 characters"`
Email string `json:"email" validate:"required,email" message:"Valid email is required"`
Age int `json:"age" validate:"gte=0,lte=100" message:"Age must be between 0 and 100"`
}
app.Post("/user", func(c fiber.Ctx) error {
user := new(User)
if err := c.Bind().Body(user); err != nil {
// Handle validation errors
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, e := range validationErrors {
// e.Field() - field name
// e.Tag() - validation tag
// e.Value() - invalid value
// e.Param() - validation parameter
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"field": e.Field(),
"error": e.Error(),
})
}
}
return err
}
return c.JSON(user)
})
```
```go title="Custom Validator Example"
// Custom validator for password strength
type PasswordValidator struct {
validate *validator.Validate
}
func (v *PasswordValidator) Validate(out any) error {
if err := v.validate.Struct(out); err != nil {
return err
}
// Custom password validation logic
if user, ok := out.(*User); ok {
if len(user.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
// Add more password validation rules here
}
return nil
}
// Usage
app := fiber.New(fiber.Config{
StructValidator: &PasswordValidator{validate: validator.New()},
})
```

View File

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

View File

@ -55,13 +55,13 @@ app.Use(logger.New(logger.Config{
}))
// 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 {
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{
Output: file,
Stream: accessLog,
}))
// Add Custom Tags
@ -79,7 +79,7 @@ app.Use(logger.New(logger.Config{
TimeZone: "Asia/Shanghai",
Done: func(c fiber.Ctx, logString []byte) {
if c.Response().StatusCode() != fiber.StatusOK {
reporter.SendToSlack(logString)
reporter.SendToSlack(logString)
}
},
}))
@ -88,6 +88,23 @@ app.Use(logger.New(logger.Config{
app.Use(logger.New(logger.Config{
DisableColors: true,
}))
// Use predefined formats
app.Use(logger.New(logger.Config{
Format: logger.FormatCommon,
}))
app.Use(logger.New(logger.Config{
Format: logger.FormatCombined,
}))
app.Use(logger.New(logger.Config{
Format: logger.FormatJSON,
}))
app.Use(logger.New(logger.Config{
Format: logger.FormatECS,
}))
```
### Use Logger Middleware with Other Loggers
@ -115,7 +132,7 @@ func main() {
// Use the logger middleware with zerolog logger
app.Use(logger.New(logger.Config{
Output: logger.LoggerToWriter(zap, log.LevelDebug),
Stream: logger.LoggerToWriter(zap, log.LevelDebug),
}))
// Define a route
@ -129,45 +146,57 @@ func main() {
```
:::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
| Property | Type | Description | Default |
|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------|
| 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` |
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` |
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |
| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` |
| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` |
| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` |
| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` |
| 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) | - |
| Property | Type | Description | Default |
| :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` |
| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` |
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
| `Format` | `string` | Defines the logging tags. See more in [Predefined Formats](#predefined-formats), or create your own using [Tags](#constants). | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` (same as `DefaultFormat`) |
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |
| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` |
| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` |
| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` |
| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` |
## Default Config
```go
var ConfigDefault = Config{
Next: nil,
Done: nil,
Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n",
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Output: os.Stdout,
DisableColors: false,
LoggerFunc: defaultLoggerInstance,
Next: nil,
Skip: nil,
Done: nil,
Format: DefaultFormat,
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Stream: os.Stdout,
BeforeHandlerFunc: beforeHandlerFunc,
LoggerFunc: defaultLoggerInstance,
enableColors: true,
}
```
## Predefined Formats
Logger provides predefined formats that you can use by name or directly by specifying the format string.
| **Format Constant** | **Format String** | **Description** |
|---------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `DefaultFormat` | `"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"` | Fiber's default logger format. |
| `CommonFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent}\n"` | Common Log Format (CLF) used in web server logs. |
| `CombinedFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent} "${referer}" "${ua}"\n"` | CLF format plus the `referer` and `user agent` fields. |
| `JSONFormat` | `"{time: ${time}, ip: ${ip}, method: ${method}, url: ${url}, status: ${status}, bytesSent: ${bytesSent}}\n"` | JSON format for structured logging. |
| `ECSFormat` | `"{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n"` | Elastic Common Schema (ECS) format for structured logging. |
## Constants
```go

View File

@ -33,6 +33,7 @@ Here's a quick overview of the changes in Fiber `v3`:
- [Filesystem](#filesystem)
- [Monitor](#monitor)
- [Healthcheck](#healthcheck)
- [🔌 Addons](#-addons)
- [📋 Migration guide](#-migration-guide)
## Drop for old Go versions
@ -915,6 +916,47 @@ func main() {
</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
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.
## 🔌 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
- [🚀 App](#-app-1)

View File

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

8
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/tinylib/msgp v1.2.5
github.com/valyala/bytebufferpool v1.0.0
github.com/valyala/fasthttp v1.59.0
golang.org/x/crypto v0.35.0
golang.org/x/crypto v0.36.0
)
require (
@ -23,8 +23,8 @@ require (
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // 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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
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.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
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/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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -14,6 +14,7 @@ import (
"os"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"sync"
@ -51,16 +52,16 @@ func getTLSConfig(ln net.Listener) *tls.Config {
}
// Copy value from pointer
if val := reflect.Indirect(pointer); val.Type() != nil {
if val := reflect.Indirect(pointer); val.IsValid() {
// 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)
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
}
// Get element from pointer
if elem := newval.Elem(); elem.Type() != nil {
if elem := newval.Elem(); elem.IsValid() {
// Cast value to *tls.Config
c, ok := elem.Interface().(*tls.Config)
if !ok {
@ -107,15 +108,15 @@ func (app *App[TCtx]) methodExist(c *DefaultCtx) bool {
methods := app.config.RequestMethods
for i := 0; i < len(methods); i++ {
// Skip original method
if c.getMethodINT() == i {
if c.getMethodInt() == i {
continue
}
// Reset stack index
c.setIndexRoute(-1)
tree, ok := app.treeStack[i][c.getTreePath()]
tree, ok := app.treeStack[i][c.treePathHash]
if !ok {
tree = app.treeStack[i][""]
tree = app.treeStack[i][0]
}
// Get stack length
lenr := len(tree) - 1
@ -152,15 +153,15 @@ func (app *App[TCtx]) methodExistCustom(c CustomCtx[TCtx]) bool {
methods := app.config.RequestMethods
for i := 0; i < len(methods); i++ {
// Skip original method
if c.getMethodINT() == i {
if c.getMethodInt() == i {
continue
}
// Reset stack index
c.setIndexRoute(-1)
tree, ok := app.treeStack[i][c.getTreePath()]
tree, ok := app.treeStack[i][c.getTreePathHash()]
if !ok {
tree = app.treeStack[i][""]
tree = app.treeStack[i][0]
}
// Get stack length
lenr := len(tree) - 1
@ -484,7 +485,7 @@ func getOffer(header []byte, isAccepted func(spec, offer string, specParams head
if len(acceptedTypes) > 1 {
// 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
@ -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.
// 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
func sortAcceptedTypes(acceptedTypes *[]acceptedType) {
if acceptedTypes == nil || len(*acceptedTypes) < 2 {
return
}
at := *acceptedTypes
func sortAcceptedTypes(at []acceptedType) {
for i := 1; i < len(at); i++ {
lo, hi := 0, i-1
for lo <= hi {
mid := (lo + hi) / 2
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 && 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) {
lo = mid + 1
} else {
@ -658,39 +654,35 @@ func getBytesImmutable(s string) []byte {
func (app *App[TCtx]) methodInt(s string) int {
// For better performance
if len(app.configured.RequestMethods) == 0 {
// TODO: Use iota instead
switch s {
case MethodGet:
return 0
return methodGet
case MethodHead:
return 1
return methodHead
case MethodPost:
return 2
return methodPost
case MethodPut:
return 3
return methodPut
case MethodDelete:
return 4
return methodDelete
case MethodConnect:
return 5
return methodConnect
case MethodOptions:
return 6
return methodOptions
case MethodTrace:
return 7
return methodTrace
case MethodPatch:
return 8
return methodPatch
default:
return -1
}
}
// For method customization
for i, v := range app.config.RequestMethods {
if s == v {
return i
}
}
return slices.Index(app.config.RequestMethods, s)
}
return -1
func (app *App) method(methodInt int) string {
return app.config.RequestMethods[methodInt]
}
// 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/*", quality: 0.5, specificity: 2, order: 1},
{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/pdf", quality: 1, specificity: 3, order: 5},
{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: "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, order: 3},
}
sortAcceptedTypes(&acceptedTypes)
sortAcceptedTypes(acceptedTypes)
require.Equal(t, []acceptedType{
{spec: "text/html", quality: 1, specificity: 3, order: 0},
{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[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1}
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/*", 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[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9}
acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10}
sortAcceptedTypes(&acceptedTypes)
sortAcceptedTypes(acceptedTypes)
}
require.Equal(b, []acceptedType{
{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)
// 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"))
require.ErrorIs(t, err, errTestConnClosed)

View File

@ -209,7 +209,7 @@ func Benchmark_Memory_Set(b *testing.B) {
b.ResetTimer()
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) {
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()
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) {
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()
for i := 0; i < b.N; i++ {
_ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark
_ = testStore.Delete("john") //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
}
}
@ -327,8 +327,8 @@ func Benchmark_Memory_SetAndDelete_Parallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = testStore.Set("john", []byte("doe"), 0) //nolint: errcheck // error not needed for benchmark
_ = testStore.Delete("john") //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
}
})
}

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
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
if IsChild() {
return
@ -366,38 +366,35 @@ func (app *App[TCtx]) startupMessage(addr string, isTLS bool, pids string, cfg L
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, strings.Repeat("-", 50)+"\n") //nolint:errcheck,revive,govet // ignore error
fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset))
fmt.Fprintf(out, strings.Repeat("-", 50)+"\n")
if host == "0.0.0.0" {
//nolint:errcheck,revive // ignore error
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",
colors.Green, colors.Reset, colors.Blue, scheme, port, colors.Reset, port)
} else {
//nolint:errcheck,revive // ignore error
fmt.Fprintf(out,
"%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)
}
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,
"%sINFO%s Total handlers count: \t%s%s%s\n",
colors.Green, colors.Reset, colors.Blue, strconv.Itoa(int(app.handlersCount)), colors.Reset)
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 {
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 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 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)
if cfg.EnablePrefork {
// 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)
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] {
fmt.Fprintf(out, "%s", pid) //nolint:errcheck,revive // ignore error
fmt.Fprintf(out, "%s", pid)
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
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
@ -473,11 +470,10 @@ func (app *App[TCtx]) printRoutesMessage() {
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, "%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, "%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)
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)
}

View File

@ -53,9 +53,9 @@ func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []any) {
buf.WriteString(level)
if len(fmtArgs) > 0 {
_, _ = fmt.Fprintf(buf, format, fmtArgs...) //nolint: errcheck // It is fine to ignore the error
_, _ = fmt.Fprintf(buf, format, fmtArgs...)
} 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

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

View File

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

View File

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

View File

@ -503,33 +503,67 @@ func Test_TokenFromContext_None(t *testing.T) {
}
func Test_TokenFromContext(t *testing.T) {
app := fiber.New()
// Wire up keyauth middleware to set TokenFromContext now
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
},
}))
// Define a test handler that checks TokenFromContext
app.Get("/", func(c fiber.Ctx) error {
return c.SendString(TokenFromContext(c))
// Test that TokenFromContext returns the correct token
t.Run("fiber.Ctx", 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
},
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString(TokenFromContext(c))
})
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))
})
req := httptest.NewRequest(fiber.MethodGet, "/", nil)
req.Header.Add("Authorization", "Basic "+CorrectKey)
// Send
res, err := app.Test(req)
require.NoError(t, err)
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)
})
// Read the response body into a string
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, CorrectKey, string(body))
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) {

View File

@ -10,16 +10,21 @@ import (
// Config defines the config for middleware.
type Config struct {
// Output is a writer where logs are written
// Stream is a writer where logs are written
//
// Default: os.Stdout
Output io.Writer
Stream io.Writer
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
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,
// and pass the log string as parameter.
//
@ -45,9 +50,23 @@ type Config struct {
timeZoneLocation *time.Location
// Format defines the logging tags
// Format defines the logging format for the middleware.
//
// Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}
// You can customize the log output by defining a format string with placeholders
// such as: ${time}, ${ip}, ${status}, ${method}, ${path}, ${latency}, ${error}, etc.
// The full list of available placeholders can be found in 'tags.go' or at
// 'https://docs.gofiber.io/api/middleware/logger/#constants'.
//
// Fiber provides predefined logging formats that can be used directly:
//
// - DefaultFormat → Uses the default log format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}"
// - CommonFormat → Uses the Apache Common Log Format (CLF): "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n"
// - CombinedFormat → Uses the Apache Combined Log Format: "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n"
// - JSONFormat → Uses the JSON log format: "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n"
// - ECSFormat → Uses the Elastic Common Schema (ECS) log format: {\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}"
// If both `Format` and `CustomFormat` are provided, the `CustomFormat` will be used, and the `Format` field will be ignored.
// If no format is specified, the default format is used:
// "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}"
Format string
// TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html
@ -98,20 +117,18 @@ type LogFunc func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (in
// ConfigDefault is the default config
var ConfigDefault = Config{
Next: nil,
Skip: nil,
Done: nil,
Format: defaultFormat,
Format: DefaultFormat,
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Output: os.Stdout,
Stream: os.Stdout,
BeforeHandlerFunc: beforeHandlerFunc,
LoggerFunc: defaultLoggerInstance,
enableColors: true,
}
// default logging format for Fiber's default logger
var defaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"
// Helper function to set default values
func configDefault(config ...Config) Config {
// Return default config if nothing provided
@ -126,6 +143,9 @@ func configDefault(config ...Config) Config {
if cfg.Next == nil {
cfg.Next = ConfigDefault.Next
}
if cfg.Skip == nil {
cfg.Skip = ConfigDefault.Skip
}
if cfg.Done == nil {
cfg.Done = ConfigDefault.Done
}
@ -141,8 +161,8 @@ func configDefault(config ...Config) Config {
if int(cfg.TimeInterval) <= 0 {
cfg.TimeInterval = ConfigDefault.TimeInterval
}
if cfg.Output == nil {
cfg.Output = ConfigDefault.Output
if cfg.Stream == nil {
cfg.Stream = ConfigDefault.Stream
}
if cfg.BeforeHandlerFunc == nil {
@ -154,7 +174,7 @@ func configDefault(config ...Config) Config {
}
// 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
}

View File

@ -15,6 +15,12 @@ import (
// default logger for fiber
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
colors := c.App().Config().ColorScheme
@ -22,7 +28,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
buf := bytebufferpool.Get()
// Default output when no custom Format or io.Writer is given
if cfg.Format == defaultFormat {
if cfg.Format == DefaultFormat {
// Format error if exist
formatErr := ""
if cfg.enableColors {
@ -91,7 +97,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
}
// Write buffer to output
writeLog(cfg.Output, buf.Bytes())
writeLog(cfg.Stream, buf.Bytes())
if cfg.Done != nil {
cfg.Done(c, buf.Bytes())
@ -125,7 +131,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
buf.WriteString(err.Error())
}
writeLog(cfg.Output, buf.Bytes())
writeLog(cfg.Stream, buf.Bytes())
if cfg.Done != nil {
cfg.Done(c, buf.Bytes())
@ -141,9 +147,9 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
func beforeHandlerFunc(cfg Config) {
// If colors are enabled, check terminal compatibility
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())) {
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
if _, err := w.Write([]byte(err.Error())); err != nil {
// 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
pid := strconv.Itoa(os.Getpid())

View File

@ -71,7 +71,7 @@ func Test_Logger(t *testing.T) {
app.Use(New(Config{
Format: "${error}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(_ fiber.Ctx) error {
@ -94,7 +94,7 @@ func Test_Logger_locals(t *testing.T) {
app.Use(New(Config{
Format: "${locals:demo}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
@ -171,6 +171,147 @@ func Test_Logger_Done(t *testing.T) {
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
func Test_Logger_ErrorTimeZone(t *testing.T) {
t.Parallel()
@ -234,7 +375,7 @@ func Test_Logger_LoggerToWriter(t *testing.T) {
app.Use("/"+level, New(Config{
Format: "${error}",
Output: LoggerToWriter(logger, tc.
Stream: LoggerToWriter(logger, tc.
level),
}))
@ -276,7 +417,7 @@ func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
Output: o,
Stream: o,
DisableColors: true,
}))
@ -293,7 +434,7 @@ func Test_Logger_ErrorOutput(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
Output: o,
Stream: o,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
@ -312,7 +453,7 @@ func Test_Logger_All(t *testing.T) {
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}",
Output: buf,
Stream: buf,
}))
// Alias colors
@ -326,6 +467,124 @@ func Test_Logger_All(t *testing.T) {
require.Equal(t, expected, buf.String())
}
func Test_Logger_CLF_Format(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: CommonFormat,
Stream: buf,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
expected := fmt.Sprintf("0.0.0.0 - - [%s] \"%s %s %s\" %d %d\n",
time.Now().Format("15:04:05"),
fiber.MethodGet, "/?foo=bar", "HTTP/1.1",
fiber.StatusNotFound,
0)
logResponse := buf.String()
require.Equal(t, expected, logResponse)
}
func Test_Logger_Combined_CLF_Format(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: CombinedFormat,
Stream: buf,
}))
const expectedUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"
const expectedReferer = "http://example.com"
req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)
req.Header.Set("Referer", expectedReferer)
req.Header.Set("User-Agent", expectedUA)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
expected := fmt.Sprintf("0.0.0.0 - - [%s] %q %d %d %q %q\n",
time.Now().Format("15:04:05"),
fmt.Sprintf("%s %s %s", fiber.MethodGet, "/?foo=bar", "HTTP/1.1"),
fiber.StatusNotFound,
0,
expectedReferer,
expectedUA)
logResponse := buf.String()
require.Equal(t, expected, logResponse)
}
func Test_Logger_Json_Format(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: JSONFormat,
Stream: buf,
}))
req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
expected := fmt.Sprintf(
"{\"time\":%q,\"ip\":%q,\"method\":%q,\"url\":%q,\"status\":%d,\"bytesSent\":%d}\n",
time.Now().Format("15:04:05"),
"0.0.0.0",
fiber.MethodGet,
"/?foo=bar",
fiber.StatusNotFound,
0,
)
logResponse := buf.String()
require.Equal(t, expected, logResponse)
}
func Test_Logger_ECS_Format(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: ECSFormat,
Stream: buf,
}))
req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
expected := fmt.Sprintf(
"{\"@timestamp\":%q,\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":%q},\"http\":{\"request\":{\"method\":%q,\"url\":%q,\"protocol\":%q},\"response\":{\"status_code\":%d,\"body\":{\"bytes\":%d}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":%q}\n",
time.Now().Format("15:04:05"),
"0.0.0.0",
fiber.MethodGet,
"/?foo=bar",
"HTTP/1.1",
fiber.StatusNotFound,
0,
fmt.Sprintf("%s %s responded with %d", fiber.MethodGet, "/?foo=bar", fiber.StatusNotFound),
)
logResponse := buf.String()
require.Equal(t, expected, logResponse)
}
func getLatencyTimeUnits() []struct {
unit string
div time.Duration
@ -358,7 +617,7 @@ func Test_Logger_WithLatency(t *testing.T) {
app := fiber.New()
logger := New(Config{
Output: buff,
Stream: buff,
Format: "${latency}",
})
app.Use(logger)
@ -403,7 +662,7 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) {
app := fiber.New()
logger := New(Config{
Output: buff,
Stream: buff,
})
app.Use(logger)
@ -453,7 +712,7 @@ func Test_Query_Params(t *testing.T) {
app.Use(New(Config{
Format: "${queryParams}",
Output: buf,
Stream: buf,
}))
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{
Format: "${resBody}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
@ -508,7 +767,7 @@ func Test_Request_Body(t *testing.T) {
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: buf,
Stream: buf,
}))
app.Post("/", func(c fiber.Ctx) error {
@ -536,7 +795,7 @@ func Test_Logger_AppendUint(t *testing.T) {
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
@ -611,7 +870,7 @@ func Test_Response_Header(t *testing.T) {
}))
app.Use(New(Config{
Format: "${respHeader:X-Request-ID}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
@ -634,7 +893,7 @@ func Test_Req_Header(t *testing.T) {
app.Use(New(Config{
Format: "${reqHeader:test}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
@ -658,7 +917,7 @@ func Test_ReqHeader_Header(t *testing.T) {
app.Use(New(Config{
Format: "${reqHeader:test}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
@ -689,7 +948,7 @@ func Test_CustomTags(t *testing.T) {
return output.WriteString(customTag)
},
},
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
@ -713,7 +972,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) {
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: buf,
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
@ -724,7 +983,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) {
for {
i++
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()
if err != nil {
break
@ -759,7 +1018,7 @@ func Test_Logger_EnableColors(t *testing.T) {
app := fiber.New()
app.Use(New(Config{
Output: o,
Stream: o,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
@ -782,7 +1041,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
@ -794,7 +1053,7 @@ func Benchmark_Logger(b *testing.B) {
b.Run("DefaultFormat", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
@ -805,7 +1064,7 @@ func Benchmark_Logger(b *testing.B) {
b.Run("DefaultFormatDisableColors", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Output: io.Discard,
Stream: io.Discard,
DisableColors: true,
}))
app.Get("/", func(c fiber.Ctx) error {
@ -819,7 +1078,7 @@ func Benchmark_Logger(b *testing.B) {
logger := fiberlog.DefaultLogger()
logger.SetOutput(io.Discard)
app.Use(New(Config{
Output: LoggerToWriter(logger, fiberlog.LevelDebug),
Stream: LoggerToWriter(logger, fiberlog.LevelDebug),
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
@ -831,7 +1090,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
@ -844,7 +1103,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe")
@ -857,7 +1116,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55)
@ -874,7 +1133,7 @@ func Benchmark_Logger(b *testing.B) {
io.Discard.Write(logString) //nolint:errcheck // ignore error
}
},
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK)
@ -886,7 +1145,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
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}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
@ -898,7 +1157,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive")
@ -908,7 +1167,7 @@ func Benchmark_Logger(b *testing.B) {
for {
i++
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()
if err != nil {
break
@ -927,7 +1186,7 @@ func Benchmark_Logger(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${resBody}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Sample response body")
@ -950,7 +1209,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
@ -962,7 +1221,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
b.Run("DefaultFormat", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
@ -975,7 +1234,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
logger := fiberlog.DefaultLogger()
logger.SetOutput(io.Discard)
app.Use(New(Config{
Output: LoggerToWriter(logger, fiberlog.LevelDebug),
Stream: LoggerToWriter(logger, fiberlog.LevelDebug),
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
@ -986,7 +1245,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
b.Run("DefaultFormatDisableColors", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Output: io.Discard,
Stream: io.Discard,
DisableColors: true,
}))
app.Get("/", func(c fiber.Ctx) error {
@ -999,7 +1258,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
@ -1012,7 +1271,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe")
@ -1025,7 +1284,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55)
@ -1042,7 +1301,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
io.Discard.Write(logString) //nolint:errcheck // ignore error
}
},
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK)
@ -1054,7 +1313,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
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}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
@ -1066,7 +1325,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive")
@ -1076,7 +1335,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
for {
i++
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()
if err != nil {
break
@ -1095,7 +1354,7 @@ func Benchmark_Logger_Parallel(b *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${resBody}",
Output: io.Discard,
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
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")
})
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.Equal(t, fiber.StatusOK, resp.StatusCode)
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")
})
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)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
@ -558,7 +564,10 @@ func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) {
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)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

129
path.go
View File

@ -7,9 +7,11 @@
package fiber
import (
"bytes"
"regexp"
"strconv"
"strings"
"sync"
"time"
"unicode"
@ -25,6 +27,12 @@ type routeParser struct {
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
type routeSegment struct {
// const information
@ -123,8 +131,6 @@ var (
parameterConstraintSeparatorChars = []byte{paramConstraintSeparator}
// list of parameter constraint data start
parameterConstraintDataStartChars = []byte{paramConstraintDataStart}
// list of parameter constraint data end
parameterConstraintDataEndChars = []byte{paramConstraintDataEnd}
// list of parameter constraint data separator
parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator}
)
@ -152,11 +158,11 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
pattern = "/" + pattern
}
patternPretty := pattern
patternPretty := []byte(pattern)
// Case-sensitive routing, all to lowercase
if !config.CaseSensitive {
patternPretty = utils.ToLower(patternPretty)
patternPretty = utils.ToLowerBytes(patternPretty)
path = utils.ToLower(path)
}
// Strict routing, remove trailing slashes
@ -164,12 +170,15 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
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
// '*' wildcard matches any path
} else if patternPretty == "/*" {
} else if string(patternPretty) == "/*" {
return true
}
@ -180,42 +189,47 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool {
}
}
// Check for a simple match
patternPretty = RemoveEscapeChar(patternPretty)
if len(patternPretty) == len(path) && patternPretty == path {
return true
}
// No match
return false
patternPretty = RemoveEscapeCharBytes(patternPretty)
return string(patternPretty) == path
}
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,
// this information is needed later when assigning the requests to the declared routes
func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser {
parser := routeParser{}
part := ""
func (parser *routeParser) parseRoute(pattern string, customConstraints ...CustomConstraint) {
var n int
var seg *routeSegment
for len(pattern) > 0 {
nextParamPosition := findNextParamPosition(pattern)
// handle the parameter part
if nextParamPosition == 0 {
processedPart, seg := parser.analyseParameterPart(pattern, customConstraints...)
parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart
n, seg = parser.analyseParameterPart(pattern, customConstraints...)
parser.params, parser.segs = append(parser.params, seg.ParamName), append(parser.segs, seg)
} else {
processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition)
parser.segs, part = append(parser.segs, seg), processedPart
n, seg = parser.analyseConstantPart(pattern, nextParamPosition)
parser.segs = append(parser.segs, seg)
}
// reduce the pattern by the processed parts
if len(part) == len(pattern) {
break
}
pattern = pattern[len(part):]
pattern = pattern[n:]
}
// mark last segment
if len(parser.segs) > 0 {
parser.segs[len(parser.segs)-1].IsLast = true
}
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
}
@ -283,7 +297,7 @@ func findNextParamPosition(pattern string) int {
}
// 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
processedPart := pattern
if nextParamPosition != -1 {
@ -291,14 +305,14 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (
processedPart = pattern[:nextParamPosition]
}
constPart := RemoveEscapeChar(processedPart)
return processedPart, &routeSegment{
return len(processedPart), &routeSegment{
Const: constPart,
Length: len(constPart),
}
}
// 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
isPlusParam := pattern[0] == plusParam
@ -317,18 +331,19 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
parameterEndPosition = 0
case parameterEndPosition == -1:
parameterEndPosition = len(pattern) - 1
case !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars):
case bytes.IndexByte(parameterDelimiterChars, pattern[parameterEndPosition+1]) == -1:
parameterEndPosition++
}
// find constraint part if exists in the parameter part and remove it
if parameterEndPosition > 0 {
parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars)
parameterConstraintEnd = findLastCharsetPosition(pattern[0:parameterEndPosition+1], parameterConstraintEndChars)
parameterConstraintEnd = strings.LastIndexByte(pattern[0:parameterEndPosition+1], paramConstraintEnd)
}
// cut params part
processedPart := pattern[0 : parameterEndPosition+1]
n := parameterEndPosition + 1
paramName := RemoveEscapeChar(GetTrimmedParam(processedPart))
// Check has constraint
@ -341,7 +356,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
for _, c := range userConstraints {
start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars)
end := findLastCharsetPosition(c, parameterConstraintDataEndChars)
end := strings.LastIndexByte(c, paramConstraintDataEnd)
// Assign constraint
if start != -1 && end != -1 {
@ -384,11 +399,11 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
// add access iterator to wildcard and plus
if isWildCard {
routeParser.wildCardCount++
paramName += strconv.Itoa(routeParser.wildCardCount)
parser.wildCardCount++
paramName += strconv.Itoa(parser.wildCardCount)
} else if isPlusParam {
routeParser.plusCount++
paramName += strconv.Itoa(routeParser.plusCount)
parser.plusCount++
paramName += strconv.Itoa(parser.plusCount)
}
segment := &routeSegment{
@ -402,17 +417,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst
segment.Constraints = constraints
}
return processedPart, 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
return n, segment
}
// findNextCharsetPosition search the next char position from the charset
@ -427,18 +432,6 @@ func findNextCharsetPosition(search string, charset []byte) int {
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
// unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern
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
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
for _, segment := range routeParser.segs {
for _, segment := range parser.segs {
partLen = len(detectionPath)
// check const segment
if !segment.IsParam {
@ -618,7 +611,7 @@ func GetTrimmedParam(param string) string {
return param[start:end]
}
// RemoveEscapeChar remove escape characters
// RemoveEscapeChar removes escape characters
func RemoveEscapeChar(word string) string {
b := []byte(word)
dst := 0
@ -632,6 +625,18 @@ func RemoveEscapeChar(word string) string {
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 {
switch constraintPart {
case ConstraintInt:

View File

@ -217,7 +217,7 @@ func Benchmark_Path_matchParams(t *testing.B) {
state = "not match"
}
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 {
// Get testCases from the original path
matchRes = true
@ -250,7 +250,7 @@ func Benchmark_RoutePatternMatch(t *testing.B) {
state = "not match"
}
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 {
// Get testCases from the original path
matchRes = true

View File

@ -178,7 +178,7 @@ func Test_Redirect_Back_WithFlashMessages(t *testing.T) {
return c.SendString("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("/")
require.NoError(t, err)
@ -225,7 +225,7 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) {
return c.SendString("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")
@ -259,7 +259,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) {
return c.SendString("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")
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")
}).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().SetBodyString("id=1&name=tom")
@ -330,7 +330,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) {
return c.SendString("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{}
writer := multipart.NewWriter(body)
@ -376,7 +376,7 @@ func Test_Redirect_parseAndClearFlashMessages(t *testing.T) {
return c.SendString("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{
{
@ -464,7 +464,7 @@ func Benchmark_Redirect_Route(b *testing.B) {
return c.JSON(c.Params("name"))
}).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.ResetTimer()
@ -491,7 +491,7 @@ func Benchmark_Redirect_Route_WithQueries(b *testing.B) {
return c.JSON(c.Params("name"))
}).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.ResetTimer()
@ -523,7 +523,7 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) {
return c.SendString("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.ResetTimer()
@ -576,7 +576,7 @@ func Benchmark_Redirect_parseAndClearFlashMessages(b *testing.B) {
return c.SendString("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)
require.NoError(b, err)
@ -618,7 +618,7 @@ func Benchmark_Redirect_processFlashMessages(b *testing.B) {
return c.SendString("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")
@ -647,7 +647,7 @@ func Benchmark_Redirect_Messages(b *testing.B) {
return c.SendString("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)
require.NoError(b, err)
@ -684,7 +684,7 @@ func Benchmark_Redirect_OldInputs(b *testing.B) {
return c.SendString("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)
require.NoError(b, err)
@ -719,7 +719,7 @@ func Benchmark_Redirect_Message(b *testing.B) {
return c.SendString("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)
require.NoError(b, err)
@ -750,7 +750,7 @@ func Benchmark_Redirect_OldInput(b *testing.B) {
return c.SendString("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)
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
}
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
tree, ok := app.treeStack[c.getMethodINT()][c.getTreePath()]
tree, ok := app.treeStack[c.getMethodInt()][c.getTreePathHash()]
if !ok {
tree = app.treeStack[c.getMethodINT()][""]
tree = app.treeStack[c.getMethodInt()][0]
}
lenr := len(tree) - 1
@ -154,6 +154,61 @@ func (app *App[TCtx]) next(c TCtx) (bool, error) { //nolint: unparam // bool par
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) {
// Acquire DefaultCtx from the pool
ctx := app.AcquireCtx(rctx)
@ -161,7 +216,7 @@ func (app *App[TCtx]) requestHandler(rctx *fasthttp.RequestCtx) {
defer app.ReleaseCtx(ctx)
// 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
return
}
@ -363,30 +418,28 @@ func (app *App[TCtx]) buildTree() *App[TCtx] {
// loop all the methods and stacks and create the prefix tree
for m := range app.config.RequestMethods {
tsMap := make(map[string][]*Route[TCtx])
tsMap := make(map[int][]*Route[TCtx])
for _, route := range app.stack[m] {
treePath := ""
if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 {
treePath = route.routeParser.segs[0].Const[:3]
treePathHash := 0
if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= maxDetectionPaths {
treePathHash = int(route.routeParser.segs[0].Const[0])<<16 |
int(route.routeParser.segs[0].Const[1])<<8 |
int(route.routeParser.segs[0].Const[2])
}
// create tree stack
tsMap[treePath] = append(tsMap[treePath], route)
tsMap[treePathHash] = append(tsMap[treePathHash], route)
}
app.treeStack[m] = tsMap
}
// loop the methods and tree stacks and add global stack and sort everything
for m := range app.config.RequestMethods {
tsMap := app.treeStack[m]
for treePart := range tsMap {
if treePart != "" {
if treePart != 0 {
// merge global tree routes in current tree stack
tsMap[treePart] = uniqueRouteStack[TCtx](append(tsMap[treePart], tsMap[""]...))
tsMap[treePart] = uniqueRouteStack[TCtx](append(tsMap[treePart], tsMap[0]...))
}
// sort tree slices with the positions
slc := tsMap[treePart]
sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos })
}
app.treeStack[m] = tsMap
}
app.routesRefreshed = false

View File

@ -600,7 +600,7 @@ func Benchmark_Router_Next(b *testing.B) {
var res bool
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()
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
func Benchmark_Route_Match(b *testing.B) {
var match bool
@ -825,7 +869,7 @@ func Benchmark_Router_Github_API(b *testing.B) {
for n := 0; n < b.N; n++ {
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)
app.ReleaseCtx(ctx)