Merge branch 'main' into feature/3098-allow-removing-registered-route

pull/3230/head
Juan Calderon-Perez 2025-03-10 21:54:36 -04:00 committed by GitHub
commit 21e42ad126
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 4718 additions and 2099 deletions

4
.github/README.md vendored
View File

@ -124,7 +124,7 @@ We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues),
## ⚠️ Limitations
- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go version 1.23.
- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber v3 has been tested with Go version 1.23 or higher.
- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem.
## 👀 Examples
@ -708,7 +708,7 @@ List of externally hosted middleware modules and maintained by the [Fiber team](
| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- |
| [contrib](https://github.com/gofiber/contrib) | Third-party middlewares |
| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. |
| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber `v3`. Go version 1.23 or higher is required. |
| [template](https://github.com/gofiber/template) | This package contains 9 template engines that can be used with Fiber. |
## 🕶️ Awesome List

9
.github/codecov.yml vendored Normal file
View File

@ -0,0 +1,9 @@
# ignore files or directories to be scanned by codecov
ignore:
- "./docs/"
coverage:
status:
project:
default:
threshold: 0.5%

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

@ -20,10 +20,10 @@ git clone https://${TOKEN}@${REPO_URL} fiber-docs
# Handle push event
if [ "$EVENT" == "push" ]; then
latest_commit=$(git rev-parse --short HEAD)
log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/)
if [[ $log_output != "" ]]; then
#log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/)
#if [[ $log_output != "" ]]; then
cp -a docs/* fiber-docs/docs/${REPO_DIR}
fi
#fi
# Handle release event
elif [ "$EVENT" == "release" ]; then

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,4 @@ jobs:
uses: golangci/golangci-lint-action@v6
with:
# NOTE: Keep this in sync with the version from .golangci.yml
version: v1.62.0
version: v1.62.2

View File

@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@v18
uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: |
**/*.md

View File

@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "22"
- name: Sync docs
run: ./.github/scripts/sync_docs.sh

View File

@ -15,7 +15,7 @@ jobs:
unit:
strategy:
matrix:
go-version: [1.23.x]
go-version: [1.23.x, 1.24.x]
platform: [ubuntu-latest, windows-latest, macos-latest, macos-13]
runs-on: ${{ matrix.platform }}
steps:
@ -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.1.2
uses: codecov/codecov-action@v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.txt

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.0 run ./...
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 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
}
```

97
app.go
View File

@ -32,7 +32,7 @@ import (
)
// Version of current fiber package
const Version = "3.0.0-beta.3"
const Version = "3.0.0-beta.4"
// Handler defines a function to serve HTTP requests.
type Handler = func(Ctx) error
@ -616,6 +616,10 @@ func (app *App) handleTrustedProxy(ipAddress string) {
// Note: It doesn't allow adding new methods, only customizing exist methods.
func (app *App) NewCtxFunc(function func(app *App) CustomCtx) {
app.newCtxFunc = function
if app.server != nil {
app.server.Handler = app.customRequestHandler
}
}
// RegisterCustomConstraint allows to register custom constraint.
@ -746,7 +750,7 @@ func (app *App) Use(args ...any) Router {
return app
}
app.register([]string{methodUse}, prefix, nil, nil, handlers...)
app.register([]string{methodUse}, prefix, nil, handlers...)
}
return app
@ -754,67 +758,67 @@ func (app *App) Use(args ...any) Router {
// Get registers a route for GET methods that requests a representation
// of the specified resource. Requests using GET should only retrieve data.
func (app *App) Get(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodGet}, path, handler, middleware...)
func (app *App) Get(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodGet}, path, handler, handlers...)
}
// Head registers a route for HEAD methods that asks for a response identical
// to that of a GET request, but without the response body.
func (app *App) Head(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodHead}, path, handler, middleware...)
func (app *App) Head(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodHead}, path, handler, handlers...)
}
// Post registers a route for POST methods that is used to submit an entity to the
// specified resource, often causing a change in state or side effects on the server.
func (app *App) Post(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodPost}, path, handler, middleware...)
func (app *App) Post(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodPost}, path, handler, handlers...)
}
// Put registers a route for PUT methods that replaces all current representations
// of the target resource with the request payload.
func (app *App) Put(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodPut}, path, handler, middleware...)
func (app *App) Put(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodPut}, path, handler, handlers...)
}
// Delete registers a route for DELETE methods that deletes the specified resource.
func (app *App) Delete(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodDelete}, path, handler, middleware...)
func (app *App) Delete(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodDelete}, path, handler, handlers...)
}
// Connect registers a route for CONNECT methods that establishes a tunnel to the
// server identified by the target resource.
func (app *App) Connect(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodConnect}, path, handler, middleware...)
func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodConnect}, path, handler, handlers...)
}
// Options registers a route for OPTIONS methods that is used to describe the
// communication options for the target resource.
func (app *App) Options(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodOptions}, path, handler, middleware...)
func (app *App) Options(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodOptions}, path, handler, handlers...)
}
// Trace registers a route for TRACE methods that performs a message loop-back
// test along the path to the target resource.
func (app *App) Trace(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodTrace}, path, handler, middleware...)
func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodTrace}, path, handler, handlers...)
}
// Patch registers a route for PATCH methods that is used to apply partial
// modifications to a resource.
func (app *App) Patch(path string, handler Handler, middleware ...Handler) Router {
return app.Add([]string{MethodPatch}, path, handler, middleware...)
func (app *App) Patch(path string, handler Handler, handlers ...Handler) Router {
return app.Add([]string{MethodPatch}, path, handler, handlers...)
}
// Add allows you to specify multiple HTTP methods to register a route.
func (app *App) Add(methods []string, path string, handler Handler, middleware ...Handler) Router {
app.register(methods, path, nil, handler, middleware...)
func (app *App) Add(methods []string, path string, handler Handler, handlers ...Handler) Router {
app.register(methods, path, nil, append([]Handler{handler}, handlers...)...)
return app
}
// All will register the handler on all HTTP methods
func (app *App) All(path string, handler Handler, middleware ...Handler) Router {
return app.Add(app.config.RequestMethods, path, handler, middleware...)
func (app *App) All(path string, handler Handler, handlers ...Handler) Router {
return app.Add(app.config.RequestMethods, path, handler, handlers...)
}
// Group is used for Routes with common prefix to define a new sub-router with optional middleware.
@ -824,7 +828,7 @@ func (app *App) All(path string, handler Handler, middleware ...Handler) Router
func (app *App) Group(prefix string, handlers ...Handler) Router {
grp := &Group{Prefix: prefix, app: app}
if len(handlers) > 0 {
app.register([]string{methodUse}, prefix, grp, nil, handlers...)
app.register([]string{methodUse}, prefix, grp, handlers...)
}
if err := app.hooks.executeOnGroupHooks(*grp); err != nil {
panic(err)
@ -868,7 +872,11 @@ func (app *App) Config() Config {
func (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476
// prepare the server for the start
app.startupProcess()
return app.requestHandler
if app.newCtxFunc != nil {
return app.customRequestHandler
}
return app.defaultRequestHandler
}
// Stack returns the raw router stack.
@ -886,6 +894,13 @@ func (app *App) HandlersCount() uint32 {
//
// Make sure the program doesn't exit and waits instead for Shutdown to return.
//
// Important: app.Listen() must be called in a separate goroutine, otherwise shutdown hooks will not work
// as Listen() is a blocking operation. Example:
//
// go app.Listen(":3000")
// // ...
// app.Shutdown()
//
// Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.
func (app *App) Shutdown() error {
return app.ShutdownWithContext(context.Background())
@ -910,17 +925,21 @@ func (app *App) ShutdownWithTimeout(timeout time.Duration) error {
//
// ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.
func (app *App) ShutdownWithContext(ctx context.Context) error {
if app.hooks != nil {
// TODO: check should be defered?
app.hooks.executeOnShutdownHooks()
}
app.mutex.Lock()
defer app.mutex.Unlock()
var err error
if app.server == nil {
return ErrNotRunning
}
return app.server.ShutdownWithContext(ctx)
// Execute the Shutdown hook
app.hooks.executeOnPreShutdownHooks()
defer app.hooks.executeOnPostShutdownHooks(err)
err = app.server.ShutdownWithContext(ctx)
return err
}
// Server returns the underlying fasthttp server
@ -933,6 +952,8 @@ func (app *App) Hooks() *Hooks {
return app.hooks
}
var ErrTestGotEmptyResponse = errors.New("test: got empty response")
// TestConfig is a struct holding Test settings
type TestConfig struct {
// Timeout defines the maximum duration a
@ -984,7 +1005,7 @@ func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, e
app.startupProcess()
// Serve conn to server
channel := make(chan error)
channel := make(chan error, 1)
go func() {
var returned bool
defer func() {
@ -1014,7 +1035,7 @@ func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, e
}
// Check for errors
if err != nil && !errors.Is(err, fasthttp.ErrGetOnly) {
if err != nil && !errors.Is(err, fasthttp.ErrGetOnly) && !errors.Is(err, errTestConnClosed) {
return nil, err
}
@ -1025,7 +1046,7 @@ func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, e
res, err := http.ReadResponse(buffer, req)
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, errors.New("test: got empty response")
return nil, ErrTestGotEmptyResponse
}
return nil, fmt.Errorf("failed to read response: %w", err)
}
@ -1057,7 +1078,11 @@ func (app *App) init() *App {
}
// fasthttp server settings
app.server.Handler = app.requestHandler
if app.newCtxFunc != nil {
app.server.Handler = app.customRequestHandler
} else {
app.server.Handler = app.defaultRequestHandler
}
app.server.Name = app.config.ServerHeader
app.server.Concurrency = app.config.Concurrency
app.server.NoDefaultDate = app.config.DisableDefaultDate

View File

@ -21,6 +21,7 @@ import (
"regexp"
"runtime"
"strings"
"sync"
"testing"
"time"
@ -57,6 +58,91 @@ func testErrorResponse(t *testing.T, err error, resp *http.Response, expectedBod
require.Equal(t, expectedBodyError, string(body), "Response body")
}
func Test_App_Test_Goroutine_Leak_Compare(t *testing.T) {
t.Parallel()
testCases := []struct {
handler Handler
name string
timeout time.Duration
sleepTime time.Duration
expectLeak bool
}{
{
name: "With timeout (potential leak)",
handler: func(c Ctx) error {
time.Sleep(300 * time.Millisecond) // Simulate time-consuming operation
return c.SendString("ok")
},
timeout: 50 * time.Millisecond, // // Short timeout to ensure triggering
sleepTime: 500 * time.Millisecond, // Wait time longer than handler execution time
expectLeak: true,
},
{
name: "Without timeout (no leak)",
handler: func(c Ctx) error {
return c.SendString("ok") // Return immediately
},
timeout: 0, // Disable timeout
sleepTime: 100 * time.Millisecond,
expectLeak: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
app := New()
// Record initial goroutine count
initialGoroutines := runtime.NumGoroutine()
t.Logf("[%s] Initial goroutines: %d", tc.name, initialGoroutines)
app.Get("/", tc.handler)
// Send 10 requests
numRequests := 10
for i := 0; i < numRequests; i++ {
req := httptest.NewRequest(MethodGet, "/", nil)
if tc.timeout > 0 {
_, err := app.Test(req, TestConfig{
Timeout: tc.timeout,
FailOnTimeout: true,
})
require.Error(t, err)
require.ErrorIs(t, err, os.ErrDeadlineExceeded)
} else if resp, err := app.Test(req); err != nil {
t.Errorf("unexpected error: %v", err)
} else {
require.Equal(t, 200, resp.StatusCode)
}
}
// Wait for normal goroutines to complete
time.Sleep(tc.sleepTime)
// Check final goroutine count
finalGoroutines := runtime.NumGoroutine()
leakedGoroutines := finalGoroutines - initialGoroutines
t.Logf("[%s] Final goroutines: %d (leaked: %d)",
tc.name, finalGoroutines, leakedGoroutines)
if tc.expectLeak {
// before fix: If blocking exists, leaked goroutines should be at least equal to request count
// after fix: If no blocking exists, leaked goroutines should be less than request count
if leakedGoroutines >= numRequests {
t.Errorf("[%s] Expected at least %d leaked goroutines, but got %d",
tc.name, numRequests, leakedGoroutines)
}
} else if leakedGoroutines >= numRequests { // If no blocking exists, leaked goroutines should be less than request count
t.Errorf("[%s] Expected less than %d leaked goroutines, but got %d",
tc.name, numRequests, leakedGoroutines)
}
})
}
}
func Test_App_MethodNotAllowed(t *testing.T) {
t.Parallel()
app := New()
@ -394,14 +480,10 @@ func Test_App_Use_Params(t *testing.T) {
require.NoError(t, err, "app.Test(req)")
require.Equal(t, 200, resp.StatusCode, "Status code")
defer func() {
if err := recover(); err != nil {
require.Equal(t, "use: invalid handler func()\n", fmt.Sprintf("%v", err))
}
}()
app.Use("/:param/*", func() {
// this should panic
require.PanicsWithValue(t, "use: invalid handler func()\n", func() {
app.Use("/:param/*", func() {
// this should panic
})
})
}
@ -581,32 +663,51 @@ func Test_App_Use_StrictRouting(t *testing.T) {
func Test_App_Add_Method_Test(t *testing.T) {
t.Parallel()
defer func() {
if err := recover(); err != nil {
require.Equal(t, "add: invalid http method JANE\n", fmt.Sprintf("%v", err))
}
}()
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
app := New(Config{
RequestMethods: methods,
})
app.Add([]string{"JOHN"}, "/doe", testEmptyHandler)
app.Add([]string{"JOHN"}, "/john", testEmptyHandler)
resp, err := app.Test(httptest.NewRequest("JOHN", "/john", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
resp, err = app.Test(httptest.NewRequest(MethodGet, "/john", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusMethodNotAllowed, resp.StatusCode, "Status code")
resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/john", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusNotImplemented, resp.StatusCode, "Status code")
// Add a new method
require.Panics(t, func() {
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
})
}
func Test_App_All_Method_Test(t *testing.T) {
t.Parallel()
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
app := New(Config{
RequestMethods: methods,
})
// Add a new method with All
app.All("/doe", testEmptyHandler)
resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
resp, err = app.Test(httptest.NewRequest(MethodGet, "/doe", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusMethodNotAllowed, resp.StatusCode, "Status code")
resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/doe", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusNotImplemented, resp.StatusCode, "Status code")
app.Add([]string{"JANE"}, "/doe", testEmptyHandler)
// Add a new method
require.Panics(t, func() {
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
})
}
// go test -run Test_App_GETOnly
@ -826,20 +927,29 @@ func Test_App_ShutdownWithTimeout(t *testing.T) {
})
ln := fasthttputil.NewInmemoryListener()
serverReady := make(chan struct{}) // Signal that the server is ready to start
go func() {
serverReady <- struct{}{}
err := app.Listener(ln)
assert.NoError(t, err)
}()
time.Sleep(1 * time.Second)
<-serverReady // Waiting for the server to be ready
// Create a connection and send a request
connReady := make(chan struct{})
go func() {
conn, err := ln.Dial()
assert.NoError(t, err)
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n"))
assert.NoError(t, err)
connReady <- struct{}{} // Signal that the request has been sent
}()
time.Sleep(1 * time.Second)
<-connReady // Waiting for the request to be sent
shutdownErr := make(chan error)
go func() {
@ -860,46 +970,130 @@ func Test_App_ShutdownWithTimeout(t *testing.T) {
func Test_App_ShutdownWithContext(t *testing.T) {
t.Parallel()
app := New()
app.Get("/", func(ctx Ctx) error {
time.Sleep(5 * time.Second)
return ctx.SendString("body")
t.Run("successful shutdown", func(t *testing.T) {
t.Parallel()
app := New()
// Fast request that should complete
app.Get("/", func(c Ctx) error {
return c.SendString("OK")
})
ln := fasthttputil.NewInmemoryListener()
serverStarted := make(chan bool, 1)
go func() {
serverStarted <- true
if err := app.Listener(ln); err != nil {
t.Errorf("Failed to start listener: %v", err)
}
}()
<-serverStarted
// Execute normal request
conn, err := ln.Dial()
require.NoError(t, err)
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
require.NoError(t, err)
// Shutdown with sufficient timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = app.ShutdownWithContext(ctx)
require.NoError(t, err, "Expected successful shutdown")
})
ln := fasthttputil.NewInmemoryListener()
t.Run("shutdown with hooks", func(t *testing.T) {
t.Parallel()
app := New()
go func() {
err := app.Listener(ln)
assert.NoError(t, err)
}()
hookOrder := make([]string, 0)
var hookMutex sync.Mutex
time.Sleep(1 * time.Second)
app.Hooks().OnPreShutdown(func() error {
hookMutex.Lock()
hookOrder = append(hookOrder, "pre")
hookMutex.Unlock()
return nil
})
go func() {
conn, err := ln.Dial()
assert.NoError(t, err)
app.Hooks().OnPostShutdown(func(_ error) error {
hookMutex.Lock()
hookOrder = append(hookOrder, "post")
hookMutex.Unlock()
return nil
})
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n"))
assert.NoError(t, err)
}()
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := app.Listener(ln); err != nil {
t.Errorf("Failed to start listener: %v", err)
}
}()
time.Sleep(1 * time.Second)
time.Sleep(100 * time.Millisecond)
shutdownErr := make(chan error)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
shutdownErr <- app.ShutdownWithContext(ctx)
}()
err := app.ShutdownWithContext(context.Background())
require.NoError(t, err)
select {
case <-time.After(5 * time.Second):
t.Fatal("idle connections not closed on shutdown")
case err := <-shutdownErr:
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded)
require.Equal(t, []string{"pre", "post"}, hookOrder, "Hooks should execute in order")
})
t.Run("timeout with long running request", func(t *testing.T) {
t.Parallel()
app := New()
requestStarted := make(chan struct{})
requestProcessing := make(chan struct{})
app.Get("/", func(c Ctx) error {
close(requestStarted)
// Wait for signal to continue processing the request
<-requestProcessing
time.Sleep(2 * time.Second)
return c.SendString("OK")
})
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := app.Listener(ln); err != nil {
t.Errorf("Failed to start listener: %v", err)
}
}()
// Ensure server is fully started
time.Sleep(100 * time.Millisecond)
// Start a long-running request
go func() {
conn, err := ln.Dial()
if err != nil {
t.Errorf("Failed to dial: %v", err)
return
}
if _, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")); err != nil {
t.Errorf("Failed to write: %v", err)
}
}()
// Wait for request to start
select {
case <-requestStarted:
// Request has started, signal to continue processing
close(requestProcessing)
case <-time.After(2 * time.Second):
t.Fatal("Request did not start in time")
}
}
// Attempt shutdown, should timeout
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
err := app.ShutdownWithContext(ctx)
require.ErrorIs(t, err, context.DeadlineExceeded)
})
}
// go test -run Test_App_Mixed_Routes_WithSameLen
@ -951,12 +1145,10 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) {
func Test_App_Group_Invalid(t *testing.T) {
t.Parallel()
defer func() {
if err := recover(); err != nil {
require.Equal(t, "use: invalid handler int\n", fmt.Sprintf("%v", err))
}
}()
New().Group("/").Use(1)
require.PanicsWithValue(t, "use: invalid handler int\n", func() {
New().Group("/").Use(1)
})
}
func Test_App_Group(t *testing.T) {
@ -1181,14 +1373,10 @@ func Test_App_Init_Error_View(t *testing.T) {
t.Parallel()
app := New(Config{Views: invalidView{}})
defer func() {
if err := recover(); err != nil {
require.Equal(t, "implement me", fmt.Sprintf("%v", err))
}
}()
err := app.config.Views.Render(nil, "", nil)
require.NoError(t, err)
require.PanicsWithValue(t, "implement me", func() {
//nolint:errcheck // not needed
_ = app.config.Views.Render(nil, "", nil)
})
}
// go test -run Test_App_Stack
@ -1472,7 +1660,7 @@ func Test_App_Test_timeout(t *testing.T) {
Timeout: 100 * time.Millisecond,
FailOnTimeout: true,
})
require.Equal(t, os.ErrDeadlineExceeded, err)
require.ErrorIs(t, err, os.ErrDeadlineExceeded)
}
func Test_App_Test_timeout_empty_response(t *testing.T) {
@ -1488,7 +1676,22 @@ func Test_App_Test_timeout_empty_response(t *testing.T) {
Timeout: 100 * time.Millisecond,
FailOnTimeout: false,
})
require.Equal(t, errors.New("test: got empty response"), err)
require.ErrorIs(t, err, ErrTestGotEmptyResponse)
}
func Test_App_Test_drop_empty_response(t *testing.T) {
t.Parallel()
app := New()
app.Get("/", func(c Ctx) error {
return c.Drop()
})
_, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), TestConfig{
Timeout: 0,
FailOnTimeout: false,
})
require.ErrorIs(t, err, ErrTestGotEmptyResponse)
}
func Test_App_SetTLSHandler(t *testing.T) {

View File

@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"mime/multipart"
"net/http/httptest"
"reflect"
"testing"
@ -886,7 +887,8 @@ func Test_Bind_Body(t *testing.T) {
reqBody := []byte(`{"name":"john"}`)
type Demo struct {
Name string `json:"name" xml:"name" form:"name" query:"name"`
Name string `json:"name" xml:"name" form:"name" query:"name"`
Names []string `json:"names" xml:"names" form:"names" query:"names"`
}
// Helper function to test compressed bodies
@ -996,6 +998,48 @@ func Test_Bind_Body(t *testing.T) {
Data []Demo `query:"data"`
}
t.Run("MultipartCollectionQueryDotNotation", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
require.NoError(t, writer.WriteField("data.0.name", "john"))
require.NoError(t, writer.WriteField("data.1.name", "doe"))
require.NoError(t, writer.Close())
c.Request().Header.SetContentType(writer.FormDataContentType())
c.Request().SetBody(buf.Bytes())
c.Request().Header.SetContentLength(len(c.Body()))
cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
})
t.Run("MultipartCollectionQuerySquareBrackets", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
require.NoError(t, writer.WriteField("data[0][name]", "john"))
require.NoError(t, writer.WriteField("data[1][name]", "doe"))
require.NoError(t, writer.Close())
c.Request().Header.SetContentType(writer.FormDataContentType())
c.Request().SetBody(buf.Bytes())
c.Request().Header.SetContentLength(len(c.Body()))
cq := new(CollectionQuery)
require.NoError(t, c.Bind().Body(cq))
require.Len(t, cq.Data, 2)
require.Equal(t, "john", cq.Data[0].Name)
require.Equal(t, "doe", cq.Data[1].Name)
})
t.Run("CollectionQuerySquareBrackets", func(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Reset()
@ -1192,9 +1236,14 @@ func Benchmark_Bind_Body_MultipartForm(b *testing.B) {
Name string `form:"name"`
}
body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
require.NoError(b, writer.WriteField("name", "john"))
require.NoError(b, writer.Close())
body := buf.Bytes()
c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=` + writer.Boundary())
c.Request().Header.SetContentLength(len(body))
d := new(Demo)
@ -1204,10 +1253,58 @@ func Benchmark_Bind_Body_MultipartForm(b *testing.B) {
for n := 0; n < b.N; n++ {
err = c.Bind().Body(d)
}
require.NoError(b, err)
require.Equal(b, "john", d.Name)
}
// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm_Nested -benchmem -count=4
func Benchmark_Bind_Body_MultipartForm_Nested(b *testing.B) {
var err error
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
type Person struct {
Name string `form:"name"`
Age int `form:"age"`
}
type Demo struct {
Name string `form:"name"`
Persons []Person `form:"persons"`
}
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
require.NoError(b, writer.WriteField("name", "john"))
require.NoError(b, writer.WriteField("persons.0.name", "john"))
require.NoError(b, writer.WriteField("persons[0][age]", "10"))
require.NoError(b, writer.WriteField("persons[1][name]", "doe"))
require.NoError(b, writer.WriteField("persons.1.age", "20"))
require.NoError(b, writer.Close())
body := buf.Bytes()
c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary=` + writer.Boundary())
c.Request().Header.SetContentLength(len(body))
d := new(Demo)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
err = c.Bind().Body(d)
}
require.NoError(b, err)
require.Equal(b, "john", d.Name)
require.Equal(b, "john", d.Persons[0].Name)
require.Equal(b, 10, d.Persons[0].Age)
require.Equal(b, "doe", d.Persons[1].Name)
require.Equal(b, 20, d.Persons[1].Age)
}
// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form_Map -benchmem -count=4
func Benchmark_Bind_Body_Form_Map(b *testing.B) {
var err error

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

@ -1,9 +1,6 @@
package binder
import (
"reflect"
"strings"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
@ -30,15 +27,7 @@ func (b *CookieBinding) Bind(req *fasthttp.Request, out any) error {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
err = formatBindData(out, data, k, v, b.EnableSplitting, false)
})
if err != nil {

View File

@ -1,8 +1,7 @@
package binder
import (
"reflect"
"strings"
"mime/multipart"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
@ -37,19 +36,7 @@ func (b *FormBinding) Bind(req *fasthttp.Request, out any) error {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
if strings.Contains(k, "[") {
k, err = parseParamSquareBrackets(k)
}
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
err = formatBindData(out, data, k, v, b.EnableSplitting, true)
})
if err != nil {
@ -61,12 +48,28 @@ func (b *FormBinding) Bind(req *fasthttp.Request, out any) error {
// bindMultipart parses the request body and returns the result.
func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
data, err := req.MultipartForm()
multipartForm, err := req.MultipartForm()
if err != nil {
return err
}
return parse(b.Name(), out, data.Value)
data := make(map[string][]string)
for key, values := range multipartForm.Value {
err = formatBindData(out, data, key, values, b.EnableSplitting, true)
if err != nil {
return err
}
}
files := make(map[string][]*multipart.FileHeader)
for key, values := range multipartForm.File {
err = formatBindData(out, files, key, values, b.EnableSplitting, true)
if err != nil {
return err
}
}
return parse(b.Name(), out, data, files)
}
// Reset resets the FormBinding binder.

View File

@ -2,6 +2,7 @@ package binder
import (
"bytes"
"io"
"mime/multipart"
"testing"
@ -57,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()
@ -93,10 +94,17 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
}
require.Equal(t, "form", b.Name())
type Post struct {
Title string `form:"title"`
}
type User struct {
Name string `form:"name"`
Names []string `form:"names"`
Age int `form:"age"`
Avatar *multipart.FileHeader `form:"avatar"`
Name string `form:"name"`
Names []string `form:"names"`
Posts []Post `form:"posts"`
Avatars []*multipart.FileHeader `form:"avatars"`
Age int `form:"age"`
}
var user User
@ -106,9 +114,31 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
mw := multipart.NewWriter(buf)
require.NoError(t, mw.WriteField("name", "john"))
require.NoError(t, mw.WriteField("names", "john"))
require.NoError(t, mw.WriteField("names", "john,eric"))
require.NoError(t, mw.WriteField("names", "doe"))
require.NoError(t, mw.WriteField("age", "42"))
require.NoError(t, mw.WriteField("posts[0][title]", "post1"))
require.NoError(t, mw.WriteField("posts[1][title]", "post2"))
require.NoError(t, mw.WriteField("posts[2][title]", "post3"))
writer, err := mw.CreateFormFile("avatar", "avatar.txt")
require.NoError(t, err)
_, err = writer.Write([]byte("avatar"))
require.NoError(t, err)
writer, err = mw.CreateFormFile("avatars", "avatar1.txt")
require.NoError(t, err)
_, err = writer.Write([]byte("avatar1"))
require.NoError(t, err)
writer, err = mw.CreateFormFile("avatars", "avatar2.txt")
require.NoError(t, err)
_, err = writer.Write([]byte("avatar2"))
require.NoError(t, err)
require.NoError(t, mw.Close())
req.Header.SetContentType(mw.FormDataContentType())
@ -118,13 +148,50 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
fasthttp.ReleaseRequest(req)
})
err := b.Bind(req, &user)
err = b.Bind(req, &user)
require.NoError(t, err)
require.Equal(t, "john", user.Name)
require.Equal(t, 42, user.Age)
require.Contains(t, user.Names, "john")
require.Contains(t, user.Names, "doe")
require.Contains(t, user.Names, "eric")
require.Len(t, user.Posts, 3)
require.Equal(t, "post1", user.Posts[0].Title)
require.Equal(t, "post2", user.Posts[1].Title)
require.Equal(t, "post3", user.Posts[2].Title)
require.NotNil(t, user.Avatar)
require.Equal(t, "avatar.txt", user.Avatar.Filename)
require.Equal(t, "application/octet-stream", user.Avatar.Header.Get("Content-Type"))
file, err := user.Avatar.Open()
require.NoError(t, err)
content, err := io.ReadAll(file)
require.NoError(t, err)
require.Equal(t, "avatar", string(content))
require.Len(t, user.Avatars, 2)
require.Equal(t, "avatar1.txt", user.Avatars[0].Filename)
require.Equal(t, "application/octet-stream", user.Avatars[0].Header.Get("Content-Type"))
file, err = user.Avatars[0].Open()
require.NoError(t, err)
content, err = io.ReadAll(file)
require.NoError(t, err)
require.Equal(t, "avatar1", string(content))
require.Equal(t, "avatar2.txt", user.Avatars[1].Filename)
require.Equal(t, "application/octet-stream", user.Avatars[1].Header.Get("Content-Type"))
file, err = user.Avatars[1].Open()
require.NoError(t, err)
content, err = io.ReadAll(file)
require.NoError(t, err)
require.Equal(t, "avatar2", string(content))
}
func Benchmark_FormBinder_BindMultipart(b *testing.B) {

View File

@ -1,9 +1,6 @@
package binder
import (
"reflect"
"strings"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
@ -21,20 +18,21 @@ func (*HeaderBinding) Name() string {
// Bind parses the request header and returns the result.
func (b *HeaderBinding) Bind(req *fasthttp.Request, out any) error {
data := make(map[string][]string)
var err error
req.Header.VisitAll(func(key, val []byte) {
if err != nil {
return
}
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
err = formatBindData(out, data, k, v, b.EnableSplitting, false)
})
if err != nil {
return err
}
return parse(b.Name(), out, data)
}

View File

@ -3,6 +3,7 @@ package binder
import (
"errors"
"fmt"
"mime/multipart"
"reflect"
"strings"
"sync"
@ -69,7 +70,7 @@ func init() {
}
// parse data into the map or struct
func parse(aliasTag string, out any, data map[string][]string) error {
func parse(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error {
ptrVal := reflect.ValueOf(out)
// Get pointer value
@ -83,11 +84,11 @@ func parse(aliasTag string, out any, data map[string][]string) error {
}
// Parse into the struct
return parseToStruct(aliasTag, out, data)
return parseToStruct(aliasTag, out, data, files...)
}
// Parse data into the struct with gorilla/schema
func parseToStruct(aliasTag string, out any, data map[string][]string) error {
// 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
defer decoderPoolMap[aliasTag].Put(schemaDecoder)
@ -95,7 +96,7 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error {
// Set alias tag
schemaDecoder.SetAliasTag(aliasTag)
if err := schemaDecoder.Decode(out, data); err != nil {
if err := schemaDecoder.Decode(out, data, files...); err != nil {
return fmt.Errorf("bind: %w", err)
}
@ -107,7 +108,6 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error {
func parseToMap(ptr any, data map[string][]string) error {
elem := reflect.TypeOf(ptr).Elem()
//nolint:exhaustive // it's not necessary to check all types
switch elem.Kind() {
case reflect.Slice:
newMap, ok := ptr.(map[string][]string)
@ -129,9 +129,10 @@ func parseToMap(ptr any, data map[string][]string) error {
newMap[k] = ""
continue
}
newMap[k] = v[len(v)-1]
}
default:
return nil // it's not necessary to check all types
}
return nil
@ -249,3 +250,55 @@ func FilterFlags(content string) string {
}
return content
}
func formatBindData[T, K any](out any, data map[string][]T, key string, value K, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay
var err error
if supportBracketNotation && strings.Contains(key, "[") {
key, err = parseParamSquareBrackets(key)
if err != nil {
return err
}
}
switch v := any(value).(type) {
case string:
dataMap, ok := any(data).(map[string][]string)
if !ok {
return fmt.Errorf("unsupported value type: %T", value)
}
assignBindData(out, dataMap, key, v, enableSplitting)
case []string:
dataMap, ok := any(data).(map[string][]string)
if !ok {
return fmt.Errorf("unsupported value type: %T", value)
}
for _, val := range v {
assignBindData(out, dataMap, key, val, enableSplitting)
}
case []*multipart.FileHeader:
for _, val := range v {
valT, ok := any(val).(T)
if !ok {
return fmt.Errorf("unsupported value type: %T", value)
}
data[key] = append(data[key], valT)
}
default:
return fmt.Errorf("unsupported value type: %T", value)
}
return err
}
func assignBindData(out any, data map[string][]string, key, value string, enableSplitting bool) { //nolint:revive // it's okay
if enableSplitting && strings.Contains(value, ",") && equalFieldType(out, reflect.Slice, key) {
values := strings.Split(value, ",")
for i := 0; i < len(values); i++ {
data[key] = append(data[key], values[i])
}
} else {
data[key] = append(data[key], value)
}
}

View File

@ -2,6 +2,7 @@ package binder
import (
"errors"
"mime/multipart"
"reflect"
"testing"
@ -9,6 +10,8 @@ import (
)
func Test_EqualFieldType(t *testing.T) {
t.Parallel()
var out int
require.False(t, equalFieldType(&out, reflect.Int, "key"))
@ -47,6 +50,8 @@ func Test_EqualFieldType(t *testing.T) {
}
func Test_ParseParamSquareBrackets(t *testing.T) {
t.Parallel()
tests := []struct {
err error
input string
@ -101,6 +106,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) {
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
result, err := parseParamSquareBrackets(tt.input)
if tt.err != nil {
require.Error(t, err)
@ -114,6 +121,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) {
}
func Test_parseToMap(t *testing.T) {
t.Parallel()
inputMap := map[string][]string{
"key1": {"value1", "value2"},
"key2": {"value3"},
@ -147,6 +156,8 @@ func Test_parseToMap(t *testing.T) {
}
func Test_FilterFlags(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected string
@ -172,8 +183,163 @@ func Test_FilterFlags(t *testing.T) {
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
result := FilterFlags(tt.input)
require.Equal(t, tt.expected, result)
})
}
}
func TestFormatBindData(t *testing.T) {
t.Parallel()
t.Run("string value with valid key", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "name", "John", false, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(data["name"]) != 1 || data["name"][0] != "John" {
t.Fatalf("expected data[\"name\"] = [John], got %v", data["name"])
}
})
t.Run("unsupported value type", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "age", 30, false, false) // int is unsupported
if err == nil {
t.Fatal("expected an error, got nil")
}
})
t.Run("bracket notation parsing error", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation
if err == nil {
t.Fatal("expected an error, got nil")
}
})
t.Run("handling multipart file headers", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]*multipart.FileHeader)
files := []*multipart.FileHeader{
{Filename: "file1.txt"},
{Filename: "file2.txt"},
}
err := formatBindData(out, data, "files", files, false, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(data["files"]) != 2 {
t.Fatalf("expected 2 files, got %d", len(data["files"]))
}
})
t.Run("type casting error", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := map[string][]int{} // Incorrect type to force a casting error
err := formatBindData(out, data, "key", "value", false, false)
require.Equal(t, "unsupported value type: string", err.Error())
})
}
func TestAssignBindData(t *testing.T) {
t.Parallel()
t.Run("splitting enabled with comma", func(t *testing.T) {
t.Parallel()
out := struct {
Colors []string `query:"colors"`
}{}
data := make(map[string][]string)
assignBindData(&out, data, "colors", "red,blue,green", true)
require.Len(t, data["colors"], 3)
})
t.Run("splitting disabled", func(t *testing.T) {
t.Parallel()
var out []string
data := make(map[string][]string)
assignBindData(out, data, "color", "red,blue", false)
require.Len(t, data["color"], 1)
})
}
func Test_parseToStruct_MismatchedData(t *testing.T) {
t.Parallel()
type User struct {
Name string `query:"name"`
Age int `query:"age"`
}
data := map[string][]string{
"name": {"John"},
"age": {"invalidAge"},
}
err := parseToStruct("query", &User{}, data)
require.Error(t, err)
require.EqualError(t, err, "bind: schema: error converting value for \"age\"")
}
func Test_formatBindData_ErrorCases(t *testing.T) {
t.Parallel()
t.Run("unsupported value type int", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "age", 30, false, false) // int is unsupported
require.Error(t, err)
require.EqualError(t, err, "unsupported value type: int")
})
t.Run("unsupported value type map", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "map", map[string]string{"key": "value"}, false, false) // map is unsupported
require.Error(t, err)
require.EqualError(t, err, "unsupported value type: map[string]string")
})
t.Run("bracket notation parsing error", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation
require.Error(t, err)
require.EqualError(t, err, "unmatched brackets")
})
t.Run("type casting error for []string", func(t *testing.T) {
t.Parallel()
out := struct{}{}
data := make(map[string][]string)
err := formatBindData(out, data, "names", 123, false, false) // invalid type for []string
require.Error(t, err)
require.EqualError(t, err, "unsupported value type: int")
})
}

View File

@ -1,9 +1,6 @@
package binder
import (
"reflect"
"strings"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
@ -30,19 +27,7 @@ func (b *QueryBinding) Bind(reqCtx *fasthttp.Request, out any) error {
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
if strings.Contains(k, "[") {
k, err = parseParamSquareBrackets(k)
}
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
err = formatBindData(out, data, k, v, b.EnableSplitting, true)
})
if err != nil {

View File

@ -1,9 +1,6 @@
package binder
import (
"reflect"
"strings"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
@ -21,20 +18,22 @@ func (*RespHeaderBinding) Name() string {
// Bind parses the response header and returns the result.
func (b *RespHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
data := make(map[string][]string)
var err error
resp.Header.VisitAll(func(key, val []byte) {
if err != nil {
return
}
k := utils.UnsafeString(key)
v := utils.UnsafeString(val)
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
values := strings.Split(v, ",")
for i := 0; i < len(values); i++ {
data[k] = append(data[k], values[i])
}
} else {
data[k] = append(data[k], v)
}
err = formatBindData(out, data, k, v, b.EnableSplitting, false)
})
if err != nil {
return err
}
return parse(b.Name(), out, data)
}

View File

@ -3,24 +3,22 @@ package client
import (
"fmt"
"io"
"math/rand"
"math/rand/v2"
"mime/multipart"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
var (
protocolCheck = regexp.MustCompile(`^https?://.*$`)
headerAccept = "Accept"
var protocolCheck = regexp.MustCompile(`^https?://.*$`)
const (
headerAccept = "Accept"
applicationJSON = "application/json"
applicationCBOR = "application/cbor"
applicationXML = "application/xml"
@ -30,25 +28,26 @@ var (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting into 63 bits
letterIdxMax = 64 / letterIdxBits // # of letter indices fitting into 64 bits
)
// randString returns a random string of length n.
func randString(n int) string {
// unsafeRandString returns a random string of length n.
func unsafeRandString(n int) string {
b := make([]byte, n)
length := len(letterBytes)
src := rand.NewSource(time.Now().UnixNano())
const length = uint64(len(letterBytes))
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
//nolint:gosec // Not a concern
for i, cache, remain := n-1, rand.Uint64(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
//nolint:gosec // Not a concern
cache, remain = rand.Uint64(), letterIdxMax
}
if idx := int(cache & int64(letterIdxMask)); idx < length {
if idx := cache & letterIdxMask; idx < length {
b[i] = letterBytes[idx]
i--
}
cache >>= int64(letterIdxBits)
cache >>= letterIdxBits
remain--
}
@ -134,7 +133,7 @@ func parserRequestHeader(c *Client, req *Request) error {
req.RawRequest.Header.SetContentType(multipartFormData)
// If boundary is default, append a random string to it.
if req.boundary == boundary {
req.boundary += randString(16)
req.boundary += unsafeRandString(16)
}
req.RawRequest.Header.SetMultipartFormBoundary(req.boundary)
default:

View File

@ -38,7 +38,7 @@ func Test_Rand_String(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := randString(tt.args)
got := unsafeRandString(tt.args)
require.Len(t, got, tt.args)
})
}

36
ctx.go
View File

@ -54,6 +54,8 @@ type DefaultCtx struct {
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
bind *Bind // Default bind reference
redirect *Redirect // Default redirect reference
req *DefaultReq // Default request api reference
res *DefaultRes // Default response api reference
values [maxParams]string // Route parameter values
viewBindMap sync.Map // Default view map to bind template engine
method string // HTTP method
@ -1347,7 +1349,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 {
@ -1369,7 +1371,7 @@ func (c *DefaultCtx) GetRouteURL(routeName string, params Map) (string, error) {
// Render a template with data and sends a text/html response.
// We support the following engines: https://github.com/gofiber/template
func (c *DefaultCtx) Render(name string, bind Map, layouts ...string) error {
func (c *DefaultCtx) Render(name string, bind any, layouts ...string) error {
// Get new buffer from pool
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
@ -1463,6 +1465,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 {
@ -1555,6 +1569,7 @@ func (c *DefaultCtx) SendFile(file string, config ...SendFile) error {
AcceptByteRange: cfg.ByteRange,
Compress: cfg.Compress,
CompressBrotli: cfg.Compress,
CompressZstd: cfg.Compress,
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
CacheDuration: cfg.CacheDuration,
SkipCache: cfg.CacheDuration < 0,
@ -1986,3 +2001,20 @@ func (c *DefaultCtx) Drop() error {
//nolint:wrapcheck // error wrapping is avoided to keep the operation lightweight and focused on connection closure.
return c.RequestCtx().Conn().Close()
}
// End immediately flushes the current response and closes the underlying connection.
func (c *DefaultCtx) End() error {
ctx := c.RequestCtx()
conn := ctx.Conn()
bw := bufio.NewWriter(conn)
if err := ctx.Response.Write(bw); err != nil {
return err
}
if err := bw.Flush(); err != nil {
return err //nolint:wrapcheck // unnecessary to wrap it
}
return conn.Close() //nolint:wrapcheck // unnecessary to wrap it
}

View File

@ -32,10 +32,14 @@ type CustomCtx interface {
func NewDefaultCtx(app *App) *DefaultCtx {
// return ctx
return &DefaultCtx{
ctx := &DefaultCtx{
// Set app reference
app: app,
}
ctx.req = &DefaultReq{ctx: ctx}
ctx.res = &DefaultRes{ctx: ctx}
return ctx
}
func (app *App) newCtx() Ctx {

View File

@ -263,8 +263,14 @@ type Ctx interface {
GetRouteURL(routeName string, params Map) (string, error)
// Render a template with data and sends a text/html response.
// We support the following engines: https://github.com/gofiber/template
Render(name string, bind Map, layouts ...string) error
Render(name string, bind any, layouts ...string) error
renderExtensions(bind any)
// Req returns a convenience type whose API is limited to operations
// on the incoming request.
Req() Req
// Res returns a convenience type whose API is limited to operations
// on the outgoing response.
Res() Res
// Route returns the matched Route struct.
Route() *Route
// SaveFile saves any multipart file to disk.
@ -350,5 +356,10 @@ type Ctx interface {
setIndexRoute(route int)
setMatched(matched bool)
setRoute(route *Route)
// Drop closes the underlying connection without sending any response headers or body.
// This can be useful for silently terminating client connections, such as in DDoS mitigation
// or when blocking access to sensitive endpoints.
Drop() error
// End immediately flushes the current response and closes the underlying connection.
End() error
}

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"))
@ -127,6 +127,35 @@ func Test_Ctx_CustomCtx(t *testing.T) {
require.Equal(t, "prefix_v3", string(body))
}
// go test -run Test_Ctx_CustomCtx
func Test_Ctx_CustomCtx_and_Method(t *testing.T) {
t.Parallel()
// Create app with custom request methods
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
app := New(Config{
RequestMethods: methods,
})
// Create custom context
app.NewCtxFunc(func(app *App) CustomCtx {
return &customCtx{
DefaultCtx: *NewDefaultCtx(app),
}
})
// Add route with custom method
app.Add([]string{"JOHN"}, "/doe", testEmptyHandler)
resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
// Add a new method
require.Panics(t, func() {
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
})
}
// go test -run Test_Ctx_Accepts_EmptyAccept
func Test_Ctx_Accepts_EmptyAccept(t *testing.T) {
t.Parallel()
@ -939,46 +968,46 @@ 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
@ -1004,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
@ -1029,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)
@ -1136,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()))
@ -1146,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()))
@ -2910,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-")
@ -3046,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")
@ -3075,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)))
}
@ -3107,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())
@ -3132,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())
@ -3753,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")
@ -3977,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)
}
@ -4878,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]"])
@ -5026,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())
}
}
@ -5059,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".
{
@ -5493,6 +5522,10 @@ func Test_GenericParseTypeUints(t *testing.T) {
value: uint(4),
str: "4",
},
{
value: ^uint(0),
str: strconv.FormatUint(uint64(^uint(0)), 10),
},
}
for _, test := range uints {
@ -5867,7 +5900,7 @@ func Test_Ctx_Drop(t *testing.T) {
// Test the Drop method
resp, err := app.Test(httptest.NewRequest(MethodGet, "/block-me", nil))
require.Error(t, err)
require.ErrorIs(t, err, ErrTestGotEmptyResponse)
require.Nil(t, resp)
// Test the no-response handler
@ -5898,7 +5931,84 @@ func Test_Ctx_DropWithMiddleware(t *testing.T) {
// Test the Drop method
resp, err := app.Test(httptest.NewRequest(MethodGet, "/block-me", nil))
require.Error(t, err)
require.ErrorIs(t, err, ErrTestGotEmptyResponse)
require.Nil(t, resp)
}
// go test -run Test_Ctx_End
func Test_Ctx_End(t *testing.T) {
app := New()
app.Get("/", func(c Ctx) error {
c.SendString("Hello, World!") //nolint:errcheck // unnecessary to check error
return c.End()
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "io.ReadAll(resp.Body)")
require.Equal(t, "Hello, World!", string(body))
}
// go test -run Test_Ctx_End_after_timeout
func Test_Ctx_End_after_timeout(t *testing.T) {
app := New()
// Early flushing handler
app.Get("/", func(c Ctx) error {
time.Sleep(2 * time.Second)
return c.End()
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
require.ErrorIs(t, err, os.ErrDeadlineExceeded)
require.Nil(t, resp)
}
// go test -run Test_Ctx_End_with_drop_middleware
func Test_Ctx_End_with_drop_middleware(t *testing.T) {
app := New()
// Middleware that will drop connections
// that persist after c.Next()
app.Use(func(c Ctx) error {
c.Next() //nolint:errcheck // unnecessary to check error
return c.Drop()
})
// Early flushing handler
app.Get("/", func(c Ctx) error {
c.SendStatus(StatusOK) //nolint:errcheck // unnecessary to check error
return c.End()
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, StatusOK, resp.StatusCode)
}
// go test -run Test_Ctx_End_after_drop
func Test_Ctx_End_after_drop(t *testing.T) {
app := New()
// Middleware that ends the request
// after c.Next()
app.Use(func(c Ctx) error {
c.Next() //nolint:errcheck // unnecessary to check error
return c.End()
})
// Early flushing handler
app.Get("/", func(c Ctx) error {
return c.Drop()
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil))
require.ErrorIs(t, err, ErrTestGotEmptyResponse)
require.Nil(t, resp)
}

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,
}
```

View File

@ -135,18 +135,18 @@ func (app *App) Route(path string) Register
```go
type Register interface {
All(handler Handler, middleware ...Handler) Register
Get(handler Handler, middleware ...Handler) Register
Head(handler Handler, middleware ...Handler) Register
Post(handler Handler, middleware ...Handler) Register
Put(handler Handler, middleware ...Handler) Register
Delete(handler Handler, middleware ...Handler) Register
Connect(handler Handler, middleware ...Handler) Register
Options(handler Handler, middleware ...Handler) Register
Trace(handler Handler, middleware ...Handler) Register
Patch(handler Handler, middleware ...Handler) Register
All(handler Handler, handlers ...Handler) Register
Get(handler Handler, handlers ...Handler) Register
Head(handler Handler, handlers ...Handler) Register
Post(handler Handler, handlers ...Handler) Register
Put(handler Handler, handlers ...Handler) Register
Delete(handler Handler, handlers ...Handler) Register
Connect(handler Handler, handlers ...Handler) Register
Options(handler Handler, handlers ...Handler) Register
Trace(handler Handler, handlers ...Handler) Register
Patch(handler Handler, handlers ...Handler) Register
Add(methods []string, handler Handler, middleware ...Handler) Register
Add(methods []string, handler Handler, handlers ...Handler) Register
Route(path string) Register
}

View File

@ -120,6 +120,38 @@ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=j
curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000
```
:::info
If you need to bind multipart file, you can use `*multipart.FileHeader`, `*[]*multipart.FileHeader` or `[]*multipart.FileHeader` as a field type.
:::
```go title="Example"
type Person struct {
Name string `form:"name"`
Pass string `form:"pass"`
Avatar *multipart.FileHeader `form:"avatar"`
}
app.Post("/", func(c fiber.Ctx) error {
p := new(Person)
if err := c.Bind().Form(p); err != nil {
return err
}
log.Println(p.Name) // john
log.Println(p.Pass) // doe
log.Println(p.Avatar.Filename) // file.txt
// ...
})
```
Run tests with the following `curl` command:
```bash
curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" -F 'avatar=@filename' localhost:3000
```
### JSON
Binds the request JSON body to a struct.

File diff suppressed because it is too large Load Diff

View File

@ -111,11 +111,9 @@ app.Listen(":8080", fiber.ListenConfig{
| <Reference id="enableprefork">EnablePrefork</Reference> | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` |
| <Reference id="enableprintroutes">EnablePrintRoutes</Reference> | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` |
| <Reference id="gracefulcontext">GracefulContext</Reference> | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` |
| <Reference id="ShutdownTimeout">ShutdownTimeout</Reference> | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 to disable the timeout and wait indefinitely. | `10 * time.Second` |
| <Reference id="ShutdownTimeout">ShutdownTimeout</Reference> | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnPostShutdown` callback. Set to 0 to disable the timeout and wait indefinitely. | `10 * time.Second` |
| <Reference id="listeneraddrfunc">ListenerAddrFunc</Reference> | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` |
| <Reference id="listenernetwork">ListenerNetwork</Reference> | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` |
| <Reference id="onshutdownerror">OnShutdownError</Reference> | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` |
| <Reference id="onshutdownsuccess">OnShutdownSuccess</Reference> | `func()` | Allows customizing success behavior when gracefully shutting down the server by given signal. | `nil` |
| <Reference id="tlsconfigfunc">TLSConfigFunc</Reference> | `func(tlsConfig *tls.Config)` | Allows customizing `tls.Config` as you want. | `nil` |
| <Reference id="autocertmanager">AutoCertManager</Reference> | `*autocert.Manager` | Manages TLS certificates automatically using the ACME protocol. Enables integration with Let's Encrypt or other ACME-compatible providers. | `nil` |
| <Reference id="tlsminversion">TLSMinVersion</Reference> | `uint16` | Allows customizing the TLS minimum version. | `tls.VersionTLS12` |
@ -230,7 +228,7 @@ Shutdown gracefully shuts down the server without interrupting any active connec
ShutdownWithTimeout will forcefully close any active connections after the timeout expires.
ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded.
ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors.
```go
func (app *App) Shutdown() error

View File

@ -15,7 +15,8 @@ With Fiber you can execute custom user functions at specific method execution po
- [OnGroupName](#ongroupname)
- [OnListen](#onlisten)
- [OnFork](#onfork)
- [OnShutdown](#onshutdown)
- [OnPreShutdown](#onpreshutdown)
- [OnPostShutdown](#onpostshutdown)
- [OnMount](#onmount)
## Constants
@ -28,7 +29,8 @@ type OnGroupHandler = func(Group) error
type OnGroupNameHandler = OnGroupHandler
type OnListenHandler = func(ListenData) error
type OnForkHandler = func(int) error
type OnShutdownHandler = func() error
type OnPreShutdownHandler = func() error
type OnPostShutdownHandler = func(error) error
type OnMountHandler = func(*App) error
```
@ -174,12 +176,20 @@ func main() {
func (h *Hooks) OnFork(handler ...OnForkHandler)
```
## OnShutdown
## OnPreShutdown
`OnShutdown` is a hook to execute user functions after shutdown.
`OnPreShutdown` is a hook to execute user functions before shutdown.
```go title="Signature"
func (h *Hooks) OnShutdown(handler ...OnShutdownHandler)
func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler)
```
## OnPostShutdown
`OnPostShutdown` is a hook to execute user functions after shutdown.
```go title="Signature"
func (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler)
```
## OnMount

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

@ -30,7 +30,7 @@ app.Use(func(c fiber.Ctx) error {
})
```
## How can i use live reload ?
## How can I use live reload?
[Air](https://github.com/air-verse/air) is a handy tool that automatically restarts your Go applications whenever the source code changes, making your development process faster and more efficient.
@ -83,12 +83,12 @@ Fiber currently supports 9 template engines in our [gofiber/template](https://do
* [ace](https://docs.gofiber.io/template/ace/)
* [amber](https://docs.gofiber.io/template/amber/)
* [django](https://docs.gofiber.io/template/django/)
* [handlebars](https://docs.gofiber.io/template/handlebars)
* [html](https://docs.gofiber.io/template/html)
* [jet](https://docs.gofiber.io/template/jet)
* [mustache](https://docs.gofiber.io/template/mustache)
* [pug](https://docs.gofiber.io/template/pug)
* [slim](https://docs.gofiber.io/template/slim)
* [handlebars](https://docs.gofiber.io/template/handlebars/)
* [html](https://docs.gofiber.io/template/html/)
* [jet](https://docs.gofiber.io/template/jet/)
* [mustache](https://docs.gofiber.io/template/mustache/)
* [pug](https://docs.gofiber.io/template/pug/)
* [slim](https://docs.gofiber.io/template/slim/)
To learn more about using Templates in Fiber, see [Templates](../guide/templates.md).
@ -99,10 +99,12 @@ If you have questions or just want to have a chat, feel free to join us via this
![](/img/support-discord.png)
## Does fiber support sub domain routing ?
## Does Fiber support subdomain routing?
Yes we do, here are some examples:
This example works v2
<details>
<summary>Example</summary>
```go
package main
@ -170,4 +172,18 @@ func main() {
}
```
</details>
If more information is needed, please refer to this issue [#750](https://github.com/gofiber/fiber/issues/750)
## How can I handle conversions between Fiber and net/http?
The `adaptor` middleware provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa.
For details on how to:
* Convert `net/http` handlers to Fiber handlers
* Convert Fiber handlers to `net/http` handlers
* Convert `fiber.Ctx` to `http.Request`
See the dedicated documentation: [Adaptor Documentation](../middleware/adaptor.md).

446
docs/extra/internal.md Normal file
View File

@ -0,0 +1,446 @@
---
title: 🏗️ Internal Architecture
description: >-
Learn about the internal architecture of Fiber, including the overall structure, request handling flow, routing, and path parsing.
sidebar_position: 3
---
## Overall Architecture
At the heart of Fiber is the **App** struct. It is responsible for configuring the server, managing a pool of Contexts (either our default implementation, **DefaultCtx**, or a usersupplied **CustomCtx**), and holding the router stack with all registered routes and groups. In addition, the App contains mount fields to support subapplications and hooks that allow developers to run custom code at key stages (e.g. when registering routes or starting the server).
```mermaid
flowchart TD
A[App]
B["Configuration (Config)"]
C[Context Pool]
D["DefaultCtx \/ CustomCtx"]
E[Router Stack]
F["Groups & Routes"]
G["MountFields (SubApps)"]
H[Hooks]
A --> B
A --> C
C --> D
A --> E
E --> F
A --> G
A --> H
```
### Explanation
- App: The central object that bootstraps and runs the Fiber server.
- Configuration (Config): Contains settings for body limits, timeouts, TLS options, routing behavior (e.g. casesensitivity, strict routing), and more.
- Context Pool: A synchronized pool from which Contexts are acquired per request. This design minimizes allocations by recycling DefaultCtx (or CustomCtx) instances.
- Router Stack: Organizes all registered routes. It is later processed into a tree structure for fast routematching.
- MountFields: Support for mounting subapplications so that large APIs can be segmented into independent routers.
- Hooks: Allow for custom behavior at critical points (e.g., on route registration, route naming, on listen, on shutdown, etc.).
## Request Processing Flow
Fibers request processing is designed for performance and minimal overhead. When an HTTP request is received by the underlying fasthttp server, the flow is as follows:
1. Request Arrival: The fasthttp server receives the HTTP request.
2. Context Acquisition: The App calls AcquireCtx() to fetch a Context from the pool.
3. Context Reset: The acquired Context is reset (via DefaultCtx.Reset()) with the new requests data.
4. Request Handling: The request handler (default or custom) is invoked.
5. Route Matching: The framework uses the next() (or nextCustom()) function to traverse the prebuilt route tree and find a matching route based on the URL and HTTP method.
6. Middleware Chain Execution: The matched routes handler chain is executed in sequence.
7. Error Handling (if required): Any errors encountered trigger the registered error handler.
8. Response Generation: The response is sent back to the client.
9. Context Release: Finally, the Context is cleaned up and returned to the pool.
```mermaid
flowchart LR
R["HTTP Request (fasthttp)"]
A["App.RequestHandler<br/>(default or custom)"]
C["Acquire Context<br/>(from Pool)"]
X["Reset Context<br/>(DefaultCtx.Reset())"]
N["Route Matching<br/>(next() \/ nextCustom())"]
M["Handler Chain Execution"]
EH["Error Handling<br/>(if needed)"]
S["HTTP Response"]
RC["Release Context<br/>(to Pool)"]
R --> A
A --> C
C --> X
X --> N
N --> M
M --> EH
EH --> S
S --> RC
```
### Additional Note
Fiber minimizes memory allocations by reusing Context objects and uses an optimized routematching algorithm to rapidly determine the correct handler chain.
## Routing & Path Parsing
Fiber allows you to register routes using helper methods (e.g. Get(), Post()) or by creating groups and subrouters. Internally, the route pattern is parsed by the parseRoute() function. This function decomposes the route string into segments:
- Constant Segments: Fixed parts of the path (e.g. /api).
- Parameter Segments: Dynamic parts that begin with a colon. For example, a route may be defined as:
/api/\:userId&lt;int&gt;
Here, the segment \:userId&lt;int&gt; is a parameter segment with a type constraint (an integer).
- Constraints: Constraints (such as int, bool, datetime, or even regular expressions) are extracted from the parameter part and stored in the routes metadata for validation at runtime.
```mermaid
flowchart TD
P["Route Pattern String<br/>(e.g., '/api/\\:userId\\&lt;int&gt;')"]
PA["parseRoute()"]
RP[routeParser]
RS["routeSegment(s)"]
C["Constraints<br/>(e.g., int, datetime, regex)"]
PARAM[Extracted Parameter Names]
P --> PA
PA --> RP
RP --> RS
RS --> C
RP --> PARAM
```
### Explanation
- parseRoute(): Takes a route string and returns a routeParser struct that includes a list of routeSegment objects.
- routeSegment: Represents a portion of the route. If it is a parameter segment, it may include constraints that determine the allowed format (for example, ensuring that a parameter is an integer).
- Extracted Parameter Names: These are later used to populate the requests Context with the actual values parsed from the URL.
## Route Matching and Parameter Extraction
When a request is processed, Fiber uses its precomputed route tree (the treeStack) to efficiently match the incoming URL against registered routes.
1. Normalization: The URL is normalized (converted to lowercase, trailing slashes trimmed) to create a “detection path.”
2. Tree Traversal: The route tree, grouped by common prefixes, is traversed based on the HTTP method.
3. Matching: Constant segments are compared exactly, while parameter segments extract dynamic values.
4. Constraint Validation: Extracted parameter values are validated against any defined constraints.
```mermaid
flowchart TD
A["Incoming Request URL<br/>(e.g., '/api/john')"]
B["Normalize URL<br/>(lowercase, trim trailing slashes)"]
C["Detection Path"]
D["Traverse Route Tree<br/>(treeStack based on method)"]
E["Match Constant Segments"]
F["Identify Parameter Segments<br/>(e.g., ':userId')"]
G["Extract Parameter Values"]
H["Validate Constraints<br/>(e.g., 'int', 'datetime', 'regex')"]
I["Route Found"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
```
### Insight
This efficient matching mechanism leverages pregrouped routes to minimize comparisons, while dynamic segments allow for flexible URL structures and runtime validation.
## Middleware Chain Execution
Once a matching route is found, Fiber executes the chain of middleware and route handlers sequentially. The process is as follows:
1. Initial Handler Execution: The first handler of the matched route is invoked.
2. Calling Next(): Each handler calls Ctx.Next() to pass control to the next handler in the chain.
3. Termination: When no further handlers remain, the chain terminates and the response is sent.
```mermaid
flowchart TD
A[Matched Route]
B[Handler 1]
C[Handler 2]
D[Handler 3]
E[Response Generation]
A --> B
B -- "Calls C via Next()" --> C
C -- "Calls D via Next()" --> D
D -- "No Next() available" --> E
```
### Explanation
- Each handler in the chain can perform operations (e.g. authentication, logging, transformation) before calling Next() to forward control.
- This sequential processing ensures that middleware are executed in the order they were registered.
- If an error occurs or a handler does not call Next(), the chain may be terminated early, and an error handler may be invoked.
### Observations
Middleware are executed in the order they are registered. This sequential design allows each handler to perform tasks such as authentication, logging, or transformation before delegating to the next handler.
## Sub-Application Mounting & Grouping
Fiber allows mounting subapplications (or subrouters) under specific path prefixes. This enables modular design of large APIs. The mounting process works as follows:
1. Defining a Mount Point: A parent application calls `App.Mount()` or a Group calls its own `mount()` method.
2. Merging Mount Fields: The subapps mount fields are updated with the prefix of the parent, and its routes are integrated into the parents routing structure.
3. Processing SubApp Routes: During startup, the parent app collects routes from mounted subapps and builds a unified route tree.
```mermaid
flowchart TD
A[Parent App]
B["Sub-App (Mounted)"]
C["Define Mount Point<br/>(e.g. \'/admin\')"]
D["Update MountFields<br/>(assign mount path)"]
E["Merge Sub-App Routes<br/>(append to Router Stack)"]
F[Generate Unified Route Tree]
A --> C
C --> B
B --> D
D --> E
E --> F
```
### Impact
This mechanism enables large APIs to be broken down into smaller, maintainable modules while still benefiting from Fibers optimized routing and request handling.
## Route Tree Building
Fiber builds a route tree (the treeStack) to optimize route matching. This involves grouping routes based on a prefix (usually the first few characters) to reduce the number of comparisons during a request.
1. Iterating Over the Router Stack: Each registered route is examined.
2. Computing the Tree Key: A key is computed from the routes normalized path (e.g. the first 3 characters).
3. Grouping Routes: Routes are added to the appropriate branch of the tree.
4. Sorting: Within each group, routes are sorted based on their registration order (or position) to ensure the correct match is found.
```mermaid
flowchart TD
A["Router Stack<br/>(All Registered Routes)"]
B["Compute Tree Key<br/>(e.g. first 3 characters)"]
C["Group Routes by Key<br/>(treeStack)"]
D["Merge Global Routes<br/>(key \'\' for global matches)"]
E[Sort Routes within Groups]
F[Optimized Route Tree]
A --> B
B --> C
C --> D
D --> E
E --> F
```
### Explanation
- Building a route tree is an optimization step that reduces the matching overhead by limiting the search space to a subset of routes that share a common prefix.
- The tree is rebuilt whenever new routes are registered, ensuring that the latest routing configuration is always used for matching.
## Context Lifecycle Management
Fiber minimizes allocations by pooling Context objects. The lifecycle of a Context is as follows:
1. **Acquisition:** When a new HTTP request arrives, a Context is retrieved from the pool via `App.AcquireCtx()`.
2. **Reset:** The acquired Context is reset with the current `fasthttp.RequestCtx` to clear previous data and initialize new requestspecific values.
3. **Processing:** The Context is passed along the middleware and handler chain.
4. **Release:** After processing the request (or when an error occurs), the Context is released back to the pool via `App.ReleaseCtx()`, making it available for reuse.
```mermaid
flowchart TD
A["HTTP Request<br/>(fasthttp)"]
B["Acquire Context<br/>(App.AcquireCtx())"]
C["Reset Context<br/>(DefaultCtx.Reset())"]
D["Process Request<br/>(Handlers & Middleware)"]
E["Error Handling<br/>(if needed)"]
F["Release Context<br/>(App.ReleaseCtx())"]
A --> B
B --> C
C --> D
D --> E
E --> F
```
### Key Benefit
Reusing Context objects significantly reduces garbage collection overhead, ensuring Fiber remains fast and memoryefficient even under heavy load.
## Preforking Mechanism
To take full advantage of multicore systems, Fiber offers a prefork mode. In this mode, the master process spawns several child processes that listen on the same port using OS features such as SO_REUSEPORT (or a fallback to SO_REUSEADDR).
```mermaid
flowchart LR
M["Master Process (App)"]
C[Child Processes]
GOMAX["Set GOMAXPROCS(1)"]
REQ[Handle HTTP Requests]
WM["watchMaster()"]
M -->|Spawns| C
C --> GOMAX
C -->|Processes| REQ
C --> WM
```
### Explanation
- Master Process: The main process determines the number of available CPU cores and spawns that many child processes.
- Child Processes: Each child sets GOMAXPROCS(1) to run on a single CPU core and listens on the shared port.
- watchMaster(): Each child process runs a watchdog routine to monitor the master process; if the master exits (or its parent process ID becomes 1 on Unixlike systems), the child terminates gracefully.
### Detailed Preforking Workflow
Fibers prefork mode uses OSlevel mechanisms to allow multiple processes to listen on the same port. Heres a more detailed look:
1. Master Process Spawning: The master process detects the number of CPU cores and spawns that many child processes.
2. Child Process Initialization: Each child process sets GOMAXPROCS(1) so that it runs on a single core.
3. Binding to Port: Child processes use packages like reuseport to bind to the same address and port.
4. Parent Monitoring: Each child runs a watchdog function (watchMaster()) to monitor the master process; if the master terminates, children exit.
5. Request Handling: Each child independently handles incoming HTTP requests.
```mermaid
flowchart TD
A[Master Process]
B[Determine CPU Cores]
C[Spawn Child Processes]
D["Child Process Initialization<br/>(GOMAXPROCS(1))"]
E["Bind to Port<br/>(reuseport)"]
F["Run watchMaster()<br/>(Monitor Parent)"]
G[Handle HTTP Requests]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
```
#### Explanation
- Preforking improves performance by allowing multiple processes to handle requests concurrently.
- Using reuseport (or a fallback) ensures that all child processes can listen on the same port without conflicts.
- The watchdog routine in each child ensures that they exit if the master process is no longer running, maintaining process integrity.
## Redirection & Flash Messages
Fibers redirection mechanism is implemented via the Redirect struct. This structure allows not only setting a new location for redirection but also passing along flash messages and old input data via a special cookie.
```mermaid
flowchart LR
R[Redirect Struct]
RP[redirectPool]
FM["Flash Messages \/ Old Inputs"]
M["Methods:<br/>To(), Route(), Back()"]
LH[Set Location Header]
CK["Flash Cookie<br/>(fiber\_flash)"]
R -->|Acquired from| RP
R --> FM
R --> M
M --> LH
FM -->|Serialized| CK
```
### Explanation
- Redirect Struct: Retrieved from a pool (to minimize allocations), it stores redirection settings such as the HTTP status code (defaulting to 302) and any flash messages.
- Flash Messages & Old Inputs: These are collected via methods like With() or WithInput() and then serialized and stored in a cookie named fiber_flash.
- Redirection Methods: The To(), Route(), and Back() methods determine the target URL and set the Location header accordingly.
### Flash Message Handling in Redirection
When performing redirections, Fiber can send flash messages or preserve old input data. This process involves:
1. Collecting Flash Data: When a redirect is initiated, developers can add flash messages via Redirect.With() or old input data via Redirect.WithInput().
2. Serialization: The flash messages and input data are serialized (using a fast marshalling method) into a byte sequence.
3. Setting a Cookie: The serialized data is stored in a special cookie (named fiber_flash) that will be sent to the client.
4. Retrieval & Clearing: On the subsequent request, the flash data is read from the cookie, deserialized, and then cleared.
```mermaid
flowchart TD
A[Initiate Redirect]
B["Add Flash Messages<br/>(With(), WithInput())"]
C[Serialize Flash Data]
D["Set Flash Cookie<br/>(\'fiber\_flash\')"]
E[Client Receives Redirect]
F[Next Request Reads Flash Cookie]
G["Deserialize & Clear Flash Data"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
```
#### Explanation
- Flash messages provide a way to pass transient data (such as notifications or error messages) to the next request after a redirect.
- The data is stored temporarily in a cookie, which is then read and cleared upon processing the next request.
- This mechanism is essential for implementing postredirectget patterns and ensuring a smooth user experience.
## Hooks, Error Handling & Context Lifecycle
### Hooks
Fiber provides a comprehensive hook system that allows you to run custom functions at key moments:
- OnRoute: Called when a route is registered.
- OnName: Invoked when a route is assigned a name.
- OnGroup: Triggered when a group is created.
- OnListen: Runs when the server starts listening.
- OnShutdown: Called during graceful shutdown.
- OnFork: Invoked when a child process is forked.
- OnMount: Used when a subapplication is mounted.
```mermaid
flowchart TD
H[Hooks]
OR[OnRoute]
ON[OnName]
OG[OnGroup]
OL[OnListen]
OS[OnShutdown]
OF[OnFork]
OM[OnMount]
H --> OR
H --> ON
H --> OG
H --> OL
H --> OS
H --> OF
H --> OM
```
#### Explanation
- Hooks provide extension points for developers and maintainers to inject custom logic without modifying the core Fiber code.
- They are executed at various stages (for example, every time a new route is registered, the OnRoute hooks are executed to allow for logging, validation, or transformation of the route).
### Error Handling & Context Lifecycle
Fibers DefaultCtx (or CustomCtx) represents the perrequest context. The lifecycle is as follows:
- Acquire: A Context is obtained from the pool at the beginning of a request.
- Processing: The context is passed along to the route handlers and middleware.
- Error Handling: If an error occurs (e.g., route not found, method not allowed, or a panic in the handler), Fiber calls the registered error handler. Errors such as ErrMethodNotAllowed or StatusNotFound are generated as needed.
- Release: Once the request is processed, the Context is released back into the pool for reuse.
```mermaid
flowchart LR
AC["Acquire Context<br/>(from Pool)"]
HP["Handle Request<br/>(Handlers & Middleware)"]
EH["Error Handling<br/>(if needed)"]
RC["Release Context<br/>(to Pool)"]
AC --> HP
HP --> EH
EH --> RC
```
#### Explanation
- This lifecycle ensures that Fiber minimizes allocations by reusing Context objects.
- Errors are propagated and handled consistently, and the context is properly reset after every request.

View File

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

View File

@ -45,9 +45,9 @@ func GetReqHeader[V any](c Ctx, key string, defaultValue ...V) V
```go title="Example"
app.Get("/search", func(c fiber.Ctx) error {
// curl -X GET http://example.com/search -H "X-Request-ID: 12345" -H "X-Request-Name: John"
GetReqHeader[int](c, "X-Request-ID") // => returns 12345 as integer.
GetReqHeader[string](c, "X-Request-Name") // => returns "John" as string.
GetReqHeader[string](c, "unknownParam", "default") // => returns "default" as string.
fiber.GetReqHeader[int](c, "X-Request-ID") // => returns 12345 as integer.
fiber.GetReqHeader[string](c, "X-Request-Name") // => returns "John" as string.
fiber.GetReqHeader[string](c, "unknownParam", "default") // => returns "default" as string.
// ...
})
```
@ -97,8 +97,8 @@ func Params[V any](c Ctx, key string, defaultValue ...V) V
```go title="Example"
app.Get("/user/:user/:id", func(c fiber.Ctx) error {
// http://example.com/user/john/25
Params[int](c, "id") // => returns 25 as integer.
Params[int](c, "unknownParam", 99) // => returns the default 99 as integer.
fiber.Params[int](c, "id") // => returns 25 as integer.
fiber.Params[int](c, "unknownParam", 99) // => returns the default 99 as integer.
// ...
return c.SendString("Hello, " + fiber.Params[string](c, "user"))
})
@ -116,9 +116,9 @@ func Query[V any](c Ctx, key string, defaultValue ...V) V
```go title="Example"
app.Get("/search", func(c fiber.Ctx) error {
// http://example.com/search?name=john&age=25
Query[string](c, "name") // => returns "john"
Query[int](c, "age") // => returns 25 as integer.
Query[string](c, "unknownParam", "default") // => returns "default" as string.
fiber.Query[string](c, "name") // => returns "john"
fiber.Query[int](c, "age") // => returns 25 as integer.
fiber.Query[string](c, "unknownParam", "default") // => returns "default" as string.
// ...
})
```

View File

@ -163,7 +163,7 @@ app.Get("/api/*", func(c fiber.Ctx) error {
### Static Files
To serve static files such as **images**, **CSS**, and **JavaScript** files, use the `Static` method with a directory path. For more information, refer to the [static middleware](./middleware/static.md).
To serve static files such as **images**, **CSS**, and **JavaScript** files, use the [static middleware](./middleware/static.md).
Use the following code to serve files in a directory named `./public`:
@ -172,12 +172,13 @@ package main
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/static"
)
func main() {
app := fiber.New()
app.Static("/", "./public")
app.Use("/", static.New("./public"))
app.Listen(":3000")
}

View File

@ -4,24 +4,34 @@ id: adaptor
# Adaptor
Converter for net/http handlers to/from Fiber request handlers, special thanks to [@arsmn](https://github.com/arsmn)!
The `adaptor` package provides utilities for converting between Fiber and `net/http`. It allows seamless integration of `net/http` handlers, middleware, and requests into Fiber applications, and vice versa.
## Signatures
## Features
| Name | Signature | Description
| :--- | :--- | :---
| HTTPHandler | `HTTPHandler(h http.Handler) fiber.Handler` | http.Handler -> fiber.Handler
| HTTPHandlerFunc | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler` | http.HandlerFunc -> fiber.Handler
| HTTPMiddleware | `HTTPHandlerFunc(mw func(http.Handler) http.Handler) fiber.Handler` | func(http.Handler) http.Handler -> fiber.Handler
| FiberHandler | `FiberHandler(h fiber.Handler) http.Handler` | fiber.Handler -> http.Handler
| FiberHandlerFunc | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | fiber.Handler -> http.HandlerFunc
| FiberApp | `FiberApp(app *fiber.App) http.HandlerFunc` | Fiber app -> http.HandlerFunc
| ConvertRequest | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)` | fiber.Ctx -> http.Request
| CopyContextToFiberContext | `CopyContextToFiberContext(context any, requestContext *fasthttp.RequestCtx)` | context.Context -> fasthttp.RequestCtx
- Convert `net/http` handlers and middleware to Fiber handlers.
- Convert Fiber handlers to `net/http` handlers.
- Convert Fiber context (`fiber.Ctx`) into an `http.Request`.
## Examples
## API Reference
### net/http to Fiber
| Name | Signature | Description |
|-----------------------------|-------------------------------------------------------------------------------|------------------------------------------------------------------|
| `HTTPHandler` | `HTTPHandler(h http.Handler) fiber.Handler` | Converts `http.Handler` to `fiber.Handler` |
| `HTTPHandlerFunc` | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler` | Converts `http.HandlerFunc` to `fiber.Handler` |
| `HTTPMiddleware` | `HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler` | Converts `http.Handler` middleware to `fiber.Handler` middleware |
| `FiberHandler` | `FiberHandler(h fiber.Handler) http.Handler` | Converts `fiber.Handler` to `http.Handler` |
| `FiberHandlerFunc` | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | Converts `fiber.Handler` to `http.HandlerFunc` |
| `FiberApp` | `FiberApp(app *fiber.App) http.HandlerFunc` | Converts an entire Fiber app to a `http.HandlerFunc` |
| `ConvertRequest` | `ConvertRequest(c fiber.Ctx, forServer bool) (*http.Request, error)` | Converts `fiber.Ctx` into a `http.Request` |
| `CopyContextToFiberContext` | `CopyContextToFiberContext(context any, requestContext *fasthttp.RequestCtx)` | Copies `context.Context` to `fasthttp.RequestCtx` |
---
## Usage Examples
### 1. Using `net/http` Handlers in Fiber
This example demonstrates how to use standard `net/http` handlers inside a Fiber application:
```go
package main
@ -29,35 +39,27 @@ package main
import (
"fmt"
"net/http"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
)
func main() {
// New fiber app
app := fiber.New()
// http.Handler -> fiber.Handler
app.Get("/", adaptor.HTTPHandler(handler(greet)))
// Convert a http.Handler to a Fiber handler
app.Get("/", adaptor.HTTPHandler(http.HandlerFunc(helloHandler)))
// http.HandlerFunc -> fiber.Handler
app.Get("/func", adaptor.HTTPHandlerFunc(greet))
// Listen on port 3000
app.Listen(":3000")
}
func handler(f http.HandlerFunc) http.Handler {
return http.HandlerFunc(f)
}
func greet(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!")
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from net/http!")
}
```
### net/http middleware to Fiber
### 2. Using `net/http` Middleware with Fiber
Middleware written for `net/http` can be used in Fiber:
```go
package main
@ -65,111 +67,119 @@ package main
import (
"log"
"net/http"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
)
func main() {
// New fiber app
app := fiber.New()
// http middleware -> fiber.Handler
app.Use(adaptor.HTTPMiddleware(logMiddleware))
// Apply a http middleware in Fiber
app.Use(adaptor.HTTPMiddleware(loggingMiddleware))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello Fiber!")
})
// Listen on port 3000
app.Listen(":3000")
}
func logMiddleware(next http.Handler) http.Handler {
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("log middleware")
log.Println("Request received")
next.ServeHTTP(w, r)
})
}
```
### Fiber Handler to net/http
### 3. Using Fiber Handlers in `net/http`
You can embed Fiber handlers inside `net/http`:
```go
package main
import (
"net/http"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
)
func main() {
// fiber.Handler -> http.Handler
http.Handle("/", adaptor.FiberHandler(greet))
// fiber.Handler -> http.HandlerFunc
http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet))
// Listen on port 3000
// Convert Fiber handler to an http.Handler
http.Handle("/", adaptor.FiberHandler(helloFiber))
// Convert Fiber handler to http.HandlerFunc
http.HandleFunc("/func", adaptor.FiberHandlerFunc(helloFiber))
http.ListenAndServe(":3000", nil)
}
func greet(c fiber.Ctx) error {
return c.SendString("Hello World!")
func helloFiber(c fiber.Ctx) error {
return c.SendString("Hello from Fiber!")
}
```
### Fiber App to net/http
### 4. Running a Fiber App in `net/http`
You can wrap a full Fiber app inside `net/http`:
```go
package main
import (
"net/http"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
)
func main() {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello from Fiber!")
})
app.Get("/greet", greet)
// Listen on port 3000
// Run Fiber inside an http server
http.ListenAndServe(":3000", adaptor.FiberApp(app))
}
func greet(c fiber.Ctx) error {
return c.SendString("Hello World!")
}
```
### Fiber Context to (net/http).Request
### 5. Converting Fiber Context (`fiber.Ctx`) to `http.Request`
If you need to use a `http.Request` inside a Fiber handler:
```go
package main
import (
"net/http"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
)
func main() {
app := fiber.New()
app.Get("/greet", greetWithHTTPReq)
// Listen on port 3000
http.ListenAndServe(":3000", adaptor.FiberApp(app))
app.Get("/request", handleRequest)
app.Listen(":3000")
}
func greetWithHTTPReq(c fiber.Ctx) error {
func handleRequest(c fiber.Ctx) error {
httpReq, err := adaptor.ConvertRequest(c, false)
if err != nil {
return err
}
return c.SendString("Request URL: " + httpReq.URL.String())
return c.SendString("Converted Request URL: " + httpReq.URL.String())
}
```
---
## Summary
The `adaptor` package allows easy interoperation between Fiber and `net/http`. You can:
- Convert handlers and middleware in both directions.
- Run Fiber apps inside `net/http`.
- Convert `fiber.Ctx` to `http.Request`.
This makes it simple to integrate Fiber with existing Go projects or migrate between frameworks as needed.

View File

@ -10,6 +10,31 @@ Request Directives<br />
`Cache-Control: no-cache` will return the up-to-date response but still caches it. You will always get a `miss` cache status.<br />
`Cache-Control: no-store` will refrain from caching. You will always get the up-to-date response.
Cacheable Status Codes<br />
This middleware caches responses with the following status codes according to RFC7231:
- `200: OK`
- `203: Non-Authoritative Information`
- `204: No Content`
- `206: Partial Content`
- `300: Multiple Choices`
- `301: Moved Permanently`
- `404: Not Found`
- `405: Method Not Allowed`
- `410: Gone`
- `414: URI Too Long`
- `501: Not Implemented`
Additionally, `418: I'm a teapot` is not originally cacheable but is cached by this middleware.
If the status code is other than these, you will always get an `unreachable` cache status.
For more information about cacheable status codes or RFC7231, please refer to the following resources:
- [Cacheable - MDN Web Docs](https://developer.mozilla.org/en-US/docs/Glossary/Cacheable)
- [RFC7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content](https://datatracker.ietf.org/doc/html/rfc7231)
## Signatures
```go

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
@ -115,7 +115,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,7 +129,7 @@ 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
@ -138,31 +138,30 @@ Writing to os.File is goroutine-safe, but if you are using a custom Output that
| 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` |
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` |
| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` |
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` |
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |
| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` |
| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` |
| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` |
| LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` |
| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` |
| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - |
| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - |
| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - |
## Default Config
```go
var ConfigDefault = Config{
Next: nil,
Skip nil,
Done: nil,
Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n",
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Output: os.Stdout,
Stream: os.Stdout,
DisableColors: false,
LoggerFunc: defaultLoggerInstance,
}

View File

@ -2,7 +2,7 @@
id: session
---
# Session Middleware for [Fiber](https://github.com/gofiber/fiber)
# Session
The `session` middleware provides session management for Fiber applications, utilizing the [Storage](https://github.com/gofiber/storage) package for multi-database support via a unified interface. By default, session data is stored in memory, but custom storage options are easily configurable (see examples below).

View File

@ -154,7 +154,7 @@ To define static routes using `Get`, append the wildcard (`*`) operator at the e
| Browse | `bool` | When set to true, enables directory browsing. | `false` |
| Download | `bool` | When set to true, enables direct download. | `false` |
| IndexNames | `[]string` | The names of the index files for serving a directory. | `[]string{"index.html"}` |
| CacheDuration | `string` | Expiration duration for inactive file handlers.<br /><br />Use a negative time.Duration to disable it. | `10 * time.Second` |
| CacheDuration | `time.Duration` | Expiration duration for inactive file handlers.<br /><br />Use a negative time.Duration to disable it. | `10 * time.Second` |
| MaxAge | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds. | `0` |
| ModifyResponse | `fiber.Handler` | ModifyResponse defines a function that allows you to alter the response. | `nil` |
| NotFoundHandler | `fiber.Handler` | NotFoundHandler defines a function to handle when the path is not found. | `nil` |

View File

@ -9,22 +9,22 @@ Registers a route bound to a specific [HTTP method](https://developer.mozilla.or
```go title="Signatures"
// HTTP methods
func (app *App) Get(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Head(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Post(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Put(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Delete(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Connect(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Options(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Trace(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Patch(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Get(path string, handler Handler, handlers ...Handler) Router
func (app *App) Head(path string, handler Handler, handlers ...Handler) Router
func (app *App) Post(path string, handler Handler, handlers ...Handler) Router
func (app *App) Put(path string, handler Handler, handlers ...Handler) Router
func (app *App) Delete(path string, handler Handler, handlers ...Handler) Router
func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router
func (app *App) Options(path string, handler Handler, handlers ...Handler) Router
func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router
func (app *App) Patch(path string, handler Handler, handlers ...Handler) Router
// Add allows you to specify a method as value
func (app *App) Add(method, path string, handler Handler, middlewares ...Handler) Router
func (app *App) Add(method, path string, handler Handler, handlers ...Handler) Router
// All will register the route on all HTTP methods
// Almost the same as app.Use but not bound to prefixes
func (app *App) All(path string, handler Handler, middlewares ...Handler) Router
func (app *App) All(path string, handler Handler, handlers ...Handler) Router
```
```go title="Examples"
@ -47,9 +47,9 @@ Can be used for middleware packages and prefix catchers. These routes will only
func (app *App) Use(args ...any) Router
// Different usage variations
func (app *App) Use(handler Handler, middlewares ...Handler) Router
func (app *App) Use(path string, handler Handler, middlewares ...Handler) Router
func (app *App) Use(paths []string, handler Handler, middlewares ...Handler) Router
func (app *App) Use(handler Handler, handlers ...Handler) Router
func (app *App) Use(path string, handler Handler, handlers ...Handler) Router
func (app *App) Use(paths []string, handler Handler, handlers ...Handler) Router
func (app *App) Use(path string, app *App) Router
```

View File

@ -16,6 +16,8 @@ In this guide, we'll walk you through the most important changes in Fiber `v3` a
Here's a quick overview of the changes in Fiber `v3`:
- [🚀 App](#-app)
- [🎣 Hooks](#-hooks)
- [🚀 Listen](#-listen)
- [🗺️ Router](#-router)
- [🧠 Context](#-context)
- [📎 Binding](#-binding)
@ -31,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
@ -158,6 +161,63 @@ app.Listen(":444", fiber.ListenConfig{
})
```
## 🎣 Hooks
We have made several changes to the Fiber hooks, including:
- Added new shutdown hooks to provide better control over the shutdown process:
- `OnPreShutdown` - Executes before the server starts shutting down
- `OnPostShutdown` - Executes after the server has shut down, receives any shutdown error
- Deprecated `OnShutdown` in favor of the new pre/post shutdown hooks
- Improved shutdown hook execution order and reliability
- Added mutex protection for hook registration and execution
Important: When using shutdown hooks, ensure app.Listen() is called in a separate goroutine:
```go
// Correct usage
go app.Listen(":3000")
// ... register shutdown hooks
app.Shutdown()
// Incorrect usage - hooks won't work
app.Listen(":3000") // This blocks
app.Shutdown() // Never reached
```
## 🚀 Listen
We have made several changes to the Fiber listen, including:
- Removed `OnShutdownError` and `OnShutdownSuccess` from `ListenerConfig` in favor of using `OnPostShutdown` hook which receives the shutdown error
```go
app := fiber.New()
// Before - using ListenerConfig callbacks
app.Listen(":3000", fiber.ListenerConfig{
OnShutdownError: func(err error) {
log.Printf("Shutdown error: %v", err)
},
OnShutdownSuccess: func() {
log.Println("Shutdown successful")
},
})
// After - using OnPostShutdown hook
app.Hooks().OnPostShutdown(func(err error) error {
if err != nil {
log.Printf("Shutdown error: %v", err)
} else {
log.Println("Shutdown successful")
}
return nil
})
go app.Listen(":3000")
```
This change simplifies the shutdown handling by consolidating the shutdown callbacks into a single hook that receives the error status.
## 🗺 Router
We have slightly adapted our router interface
@ -341,17 +401,19 @@ testConfig := fiber.TestConfig{
- **String**: Similar to Express.js, converts a value to a string.
- **ViewBind**: Binds data to a view, replacing the old `Bind` method.
- **CBOR**: Introducing [CBOR](https://cbor.io/) binary encoding format for both request & response body. CBOR is a binary data serialization format which is both compact and efficient, making it ideal for use in web applications.
- **Drop**: Terminates the client connection silently without sending any HTTP headers or response body. This can be used for scenarios where you want to block certain requests without notifying the client, such as mitigating DDoS attacks or protecting sensitive endpoints from unauthorized access.
- **End**: Similar to Express.js, immediately flushes the current response and closes the underlying connection.
### Removed Methods
- **AllParams**: Use `c.Bind().URL()` instead.
- **AllParams**: Use `c.Bind().URI()` instead.
- **ParamsInt**: Use `Params` with generic types.
- **QueryBool**: Use `Query` with generic types.
- **QueryFloat**: Use `Query` with generic types.
- **QueryInt**: Use `Query` with generic types.
- **BodyParser**: Use `c.Bind().Body()` instead.
- **CookieParser**: Use `c.Bind().Cookie()` instead.
- **ParamsParser**: Use `c.Bind().URL()` instead.
- **ParamsParser**: Use `c.Bind().URI()` instead.
- **RedirectToRoute**: Use `c.Redirect().Route()` instead.
- **RedirectBack**: Use `c.Redirect().Back()` instead.
- **ReqHeaderParser**: Use `c.Bind().Header()` instead.
@ -403,6 +465,72 @@ app.Get("/sse", func(c fiber.Ctx) {
You can find more details about this feature in [/docs/api/ctx.md](./api/ctx.md).
### Drop
In v3, we introduced support to silently terminate requests through `Drop`.
```go
func (c Ctx) Drop()
```
With this method, you can:
- Block certain requests without notifying the client to mitigate DDoS attacks
- Protect sensitive endpoints from unauthorized access without leaking errors.
:::caution
While this feature adds the ability to drop connections, it is still **highly recommended** to use additional
measures (such as **firewalls**, **proxies**, etc.) to further protect your server endpoints by blocking
malicious connections before the server establishes a connection.
:::
```go
app.Get("/", func(c fiber.Ctx) error {
if c.IP() == "192.168.1.1" {
return c.Drop()
}
return c.SendString("Hello World!")
})
```
You can find more details about this feature in [/docs/api/ctx.md](./api/ctx.md).
### End
In v3, we introduced a new method to match the Express.js API's `res.end()` method.
```go
func (c Ctx) End()
```
With this method, you can:
- Stop middleware from controlling the connection after a handler further up the method chain
by immediately flushing the current response and closing the connection.
- Use `return c.End()` as an alternative to `return nil`
```go
app.Use(func (c fiber.Ctx) error {
err := c.Next()
if err != nil {
log.Println("Got error: %v", err)
return c.SendString(err.Error()) // Will be unsuccessful since the response ended below
}
return nil
})
app.Get("/hello", func (c fiber.Ctx) error {
query := c.Query("name", "")
if query == "" {
c.SendString("You don't have a name?")
c.End() // Closes the underlying connection
return errors.New("No name provided")
}
return c.SendString("Hello, " + query + "!")
})
```
---
## 🌎 Client package
@ -419,6 +547,7 @@ Fiber v3 introduces a new binding mechanism that simplifies the process of bindi
- Unified binding from URL parameters, query parameters, headers, and request bodies.
- Support for custom binders and constraints.
- Improved error handling and validation.
- Support multipart file binding for `*multipart.FileHeader`, `*[]*multipart.FileHeader`, and `[]*multipart.FileHeader` field types.
<details>
<summary>Example</summary>
@ -497,7 +626,7 @@ func main() {
app := fiber.New()
app.Get("/convert", func(c fiber.Ctx) error {
value, err := Convert[string](c.Query("value"), strconv.Atoi, 0)
value, err := fiber.Convert[string](c.Query("value"), strconv.Atoi, 0)
if err != nil {
return c.Status(fiber.StatusBadRequest).SendString(err.Error())
}
@ -575,7 +704,7 @@ func main() {
app := fiber.New()
app.Get("/params/:id", func(c fiber.Ctx) error {
id := Params[int](c, "id", 0)
id := fiber.Params[int](c, "id", 0)
return c.JSON(id)
})
@ -607,7 +736,7 @@ func main() {
app := fiber.New()
app.Get("/query", func(c fiber.Ctx) error {
age := Query[int](c, "age", 0)
age := fiber.Query[int](c, "age", 0)
return c.JSON(age)
})
@ -640,7 +769,7 @@ func main() {
app := fiber.New()
app.Get("/header", func(c fiber.Ctx) error {
userAgent := GetReqHeader[string](c, "User-Agent", "Unknown")
userAgent := fiber.GetReqHeader[string](c, "User-Agent", "Unknown")
return c.JSON(userAgent)
})
@ -696,7 +825,8 @@ The adaptor middleware has been significantly optimized for performance and effi
### Cache
We are excited to introduce a new option in our caching middleware: Cache Invalidator. This feature provides greater control over cache management, allowing you to define a custom conditions for invalidating cache entries.
We are excited to introduce a new option in our caching middleware: Cache Invalidator. This feature provides greater control over cache management, allowing you to define a custom conditions for invalidating cache entries.
Additionally, the caching middleware has been optimized to avoid caching non-cacheable status codes, as defined by the [HTTP standards](https://datatracker.ietf.org/doc/html/rfc7231#section-6.1). This improvement enhances cache accuracy and reduces unnecessary cache storage usage.
### CORS
@ -782,6 +912,31 @@ 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>
### Filesystem
We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware.
@ -810,6 +965,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)
@ -1056,7 +1264,7 @@ The `Parser` section in Fiber v3 has undergone significant changes to improve fu
</details>
2. **ParamsParser**: Use `c.Bind().URL()` instead of `c.ParamsParser()`.
2. **ParamsParser**: Use `c.Bind().URI()` instead of `c.ParamsParser()`.
<details>
<summary>Example</summary>
@ -1076,7 +1284,7 @@ The `Parser` section in Fiber v3 has undergone significant changes to improve fu
// After
app.Get("/user/:id", func(c fiber.Ctx) error {
var params Params
if err := c.Bind().URL(&params); err != nil {
if err := c.Bind().URI(&params); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(params)

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

17
go.mod
View File

@ -1,18 +1,18 @@
module github.com/gofiber/fiber/v3
go 1.23
go 1.23.0
require (
github.com/gofiber/schema v1.2.0
github.com/gofiber/schema v1.3.0
github.com/gofiber/utils/v2 v2.0.0-beta.7
github.com/google/uuid v1.6.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.20
github.com/stretchr/testify v1.10.0
github.com/tinylib/msgp v1.2.5
github.com/valyala/bytebufferpool v1.0.0
github.com/valyala/fasthttp v1.58.0
golang.org/x/crypto v0.31.0
github.com/valyala/fasthttp v1.59.0
golang.org/x/crypto v0.36.0
)
require (
@ -22,10 +22,9 @@ require (
github.com/klauspost/compress v1.17.11 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/net v0.35.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
)

32
go.sum
View File

@ -4,17 +4,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q=
github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
@ -27,24 +26,21 @@ github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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

@ -97,7 +97,7 @@ func (grp *Group) Use(args ...any) Router {
return grp
}
grp.app.register([]string{methodUse}, getGroupPath(grp.Prefix, prefix), grp, nil, handlers...)
grp.app.register([]string{methodUse}, getGroupPath(grp.Prefix, prefix), grp, handlers...)
}
if !grp.anyRouteDefined {
@ -109,60 +109,60 @@ func (grp *Group) Use(args ...any) Router {
// Get registers a route for GET methods that requests a representation
// of the specified resource. Requests using GET should only retrieve data.
func (grp *Group) Get(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodGet}, path, handler, middleware...)
func (grp *Group) Get(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodGet}, path, handler, handlers...)
}
// Head registers a route for HEAD methods that asks for a response identical
// to that of a GET request, but without the response body.
func (grp *Group) Head(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodHead}, path, handler, middleware...)
func (grp *Group) Head(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodHead}, path, handler, handlers...)
}
// Post registers a route for POST methods that is used to submit an entity to the
// specified resource, often causing a change in state or side effects on the server.
func (grp *Group) Post(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodPost}, path, handler, middleware...)
func (grp *Group) Post(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodPost}, path, handler, handlers...)
}
// Put registers a route for PUT methods that replaces all current representations
// of the target resource with the request payload.
func (grp *Group) Put(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodPut}, path, handler, middleware...)
func (grp *Group) Put(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodPut}, path, handler, handlers...)
}
// Delete registers a route for DELETE methods that deletes the specified resource.
func (grp *Group) Delete(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodDelete}, path, handler, middleware...)
func (grp *Group) Delete(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodDelete}, path, handler, handlers...)
}
// Connect registers a route for CONNECT methods that establishes a tunnel to the
// server identified by the target resource.
func (grp *Group) Connect(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodConnect}, path, handler, middleware...)
func (grp *Group) Connect(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodConnect}, path, handler, handlers...)
}
// Options registers a route for OPTIONS methods that is used to describe the
// communication options for the target resource.
func (grp *Group) Options(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodOptions}, path, handler, middleware...)
func (grp *Group) Options(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodOptions}, path, handler, handlers...)
}
// Trace registers a route for TRACE methods that performs a message loop-back
// test along the path to the target resource.
func (grp *Group) Trace(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodTrace}, path, handler, middleware...)
func (grp *Group) Trace(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodTrace}, path, handler, handlers...)
}
// Patch registers a route for PATCH methods that is used to apply partial
// modifications to a resource.
func (grp *Group) Patch(path string, handler Handler, middleware ...Handler) Router {
return grp.Add([]string{MethodPatch}, path, handler, middleware...)
func (grp *Group) Patch(path string, handler Handler, handlers ...Handler) Router {
return grp.Add([]string{MethodPatch}, path, handler, handlers...)
}
// Add allows you to specify multiple HTTP methods to register a route.
func (grp *Group) Add(methods []string, path string, handler Handler, middleware ...Handler) Router {
grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, handler, middleware...)
func (grp *Group) Add(methods []string, path string, handler Handler, handlers ...Handler) Router {
grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, append([]Handler{handler}, handlers...)...)
if !grp.anyRouteDefined {
grp.anyRouteDefined = true
}
@ -171,8 +171,8 @@ func (grp *Group) Add(methods []string, path string, handler Handler, middleware
}
// All will register the handler on all HTTP methods
func (grp *Group) All(path string, handler Handler, middleware ...Handler) Router {
_ = grp.Add(grp.app.config.RequestMethods, path, handler, middleware...)
func (grp *Group) All(path string, handler Handler, handlers ...Handler) Router {
_ = grp.Add(grp.app.config.RequestMethods, path, handler, handlers...)
return grp
}
@ -183,7 +183,7 @@ func (grp *Group) All(path string, handler Handler, middleware ...Handler) Route
func (grp *Group) Group(prefix string, handlers ...Handler) Router {
prefix = getGroupPath(grp.Prefix, prefix)
if len(handlers) > 0 {
grp.app.register([]string{methodUse}, prefix, grp, nil, handlers...)
grp.app.register([]string{methodUse}, prefix, grp, handlers...)
}
// Create new group

View File

@ -192,12 +192,10 @@ func (app *App) methodExistCustom(c CustomCtx) bool {
// uniqueRouteStack drop all not unique routes from the slice
func uniqueRouteStack(stack []*Route) []*Route {
var unique []*Route
m := make(map[*Route]int)
m := make(map[*Route]struct{})
for _, v := range stack {
if _, ok := m[v]; !ok {
// Unique key found. Record position and collect
// in result.
m[v] = len(unique)
m[v] = struct{}{}
unique = append(unique, v)
}
}
@ -323,28 +321,23 @@ func getSplicedStrList(headerValue string, dst []string) []string {
return nil
}
var (
index int
character rune
lastElementEndsAt int
insertIndex int
)
for index, character = range headerValue + "$" {
if character == ',' || index == len(headerValue) {
if insertIndex >= len(dst) {
oldSlice := dst
dst = make([]string, len(dst)+(len(dst)>>1)+2)
copy(dst, oldSlice)
}
dst[insertIndex] = utils.TrimLeft(headerValue[lastElementEndsAt:index], ' ')
lastElementEndsAt = index + 1
insertIndex++
dst = dst[:0]
segmentStart := 0
isLeadingSpace := true
for i, c := range headerValue {
switch {
case c == ',':
dst = append(dst, headerValue[segmentStart:i])
segmentStart = i + 1
isLeadingSpace = true
case c == ' ' && isLeadingSpace:
segmentStart = i + 1
default:
isLeadingSpace = false
}
}
dst = append(dst, headerValue[segmentStart:])
if len(dst) > insertIndex {
dst = dst[:insertIndex]
}
return dst
}
@ -490,7 +483,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
@ -518,19 +511,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 {
@ -612,6 +600,8 @@ func isNoCache(cacheControl string) bool {
return true
}
var errTestConnClosed = errors.New("testConn is closed")
type testConn struct {
r bytes.Buffer
w bytes.Buffer
@ -631,7 +621,7 @@ func (c *testConn) Write(b []byte) (int, error) {
defer c.Unlock()
if c.isClosed {
return 0, errors.New("testConn is closed")
return 0, errTestConnClosed
}
return c.w.Write(b) //nolint:wrapcheck // This must not be wrapped
}
@ -726,15 +716,6 @@ func IsMethodIdempotent(m string) bool {
}
}
func IndexRune(str string, needle int32) bool {
for _, b := range str {
if b == needle {
return true
}
}
return false
}
// Convert a string value to a specified type, handling errors and optional default values.
func Convert[T any](value string, convertor func(string) (T, error), defaultValue ...T) (T, error) {
converted, err := convertor(value)
@ -803,7 +784,7 @@ func genericParseType[V GenericType](str string, v V, defaultValue ...V) V {
case int64:
return genericParseInt[V](str, 64, func(i int64) V { return assertValueType[V, int64](i) }, defaultValue...)
case uint:
return genericParseUint[V](str, 32, func(i uint64) V { return assertValueType[V, uint](uint(i)) }, defaultValue...)
return genericParseUint[V](str, 0, func(i uint64) V { return assertValueType[V, uint](uint(i)) }, defaultValue...)
case uint8:
return genericParseUint[V](str, 8, func(i uint64) V { return assertValueType[V, uint8](uint8(i)) }, defaultValue...)
case uint16:

View File

@ -303,6 +303,26 @@ func Test_Utils_GetSplicedStrList(t *testing.T) {
headerValue: "gzip,",
expectedList: []string{"gzip", ""},
},
{
description: "has a space between words",
headerValue: " foo bar, hello world",
expectedList: []string{"foo bar", "hello world"},
},
{
description: "single comma",
headerValue: ",",
expectedList: []string{"", ""},
},
{
description: "multiple comma",
headerValue: ",,",
expectedList: []string{"", "", ""},
},
{
description: "comma with space",
headerValue: ", ,",
expectedList: []string{"", "", ""},
},
}
for _, tc := range testCases {
@ -334,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},
@ -343,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},
@ -370,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)
@ -394,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},
@ -548,7 +568,7 @@ func Test_Utils_TestConn_Closed_Write(t *testing.T) {
// Close early, write should fail
conn.Close() //nolint:errcheck, revive // It is fine to ignore the error here
_, err = conn.Write([]byte("Response 2\n"))
require.Error(t, err)
require.ErrorIs(t, err, errTestConnClosed)
res := make([]byte, 11)
_, err = conn.w.Read(res)
@ -625,13 +645,13 @@ func Benchmark_SlashRecognition(b *testing.B) {
}
require.True(b, result)
})
b.Run("IndexRune", func(b *testing.B) {
b.Run("strings.ContainsRune", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
result = false
c := int32(slashDelimiter)
for i := 0; i < b.N; i++ {
result = IndexRune(search, c)
result = strings.ContainsRune(search, c)
}
require.True(b, result)
})

View File

@ -6,14 +6,15 @@ import (
// OnRouteHandler Handlers define a function to create hooks for Fiber.
type (
OnRouteHandler = func(Route) error
OnNameHandler = OnRouteHandler
OnGroupHandler = func(Group) error
OnGroupNameHandler = OnGroupHandler
OnListenHandler = func(ListenData) error
OnShutdownHandler = func() error
OnForkHandler = func(int) error
OnMountHandler = func(*App) error
OnRouteHandler = func(Route) error
OnNameHandler = OnRouteHandler
OnGroupHandler = func(Group) error
OnGroupNameHandler = OnGroupHandler
OnListenHandler = func(ListenData) error
OnPreShutdownHandler = func() error
OnPostShutdownHandler = func(error) error
OnForkHandler = func(int) error
OnMountHandler = func(*App) error
)
// Hooks is a struct to use it with App.
@ -22,14 +23,15 @@ type Hooks struct {
app *App
// Hooks
onRoute []OnRouteHandler
onName []OnNameHandler
onGroup []OnGroupHandler
onGroupName []OnGroupNameHandler
onListen []OnListenHandler
onShutdown []OnShutdownHandler
onFork []OnForkHandler
onMount []OnMountHandler
onRoute []OnRouteHandler
onName []OnNameHandler
onGroup []OnGroupHandler
onGroupName []OnGroupNameHandler
onListen []OnListenHandler
onPreShutdown []OnPreShutdownHandler
onPostShutdown []OnPostShutdownHandler
onFork []OnForkHandler
onMount []OnMountHandler
}
// ListenData is a struct to use it with OnListenHandler
@ -41,15 +43,16 @@ type ListenData struct {
func newHooks(app *App) *Hooks {
return &Hooks{
app: app,
onRoute: make([]OnRouteHandler, 0),
onGroup: make([]OnGroupHandler, 0),
onGroupName: make([]OnGroupNameHandler, 0),
onName: make([]OnNameHandler, 0),
onListen: make([]OnListenHandler, 0),
onShutdown: make([]OnShutdownHandler, 0),
onFork: make([]OnForkHandler, 0),
onMount: make([]OnMountHandler, 0),
app: app,
onRoute: make([]OnRouteHandler, 0),
onGroup: make([]OnGroupHandler, 0),
onGroupName: make([]OnGroupNameHandler, 0),
onName: make([]OnNameHandler, 0),
onListen: make([]OnListenHandler, 0),
onPreShutdown: make([]OnPreShutdownHandler, 0),
onPostShutdown: make([]OnPostShutdownHandler, 0),
onFork: make([]OnForkHandler, 0),
onMount: make([]OnMountHandler, 0),
}
}
@ -96,10 +99,17 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) {
h.app.mutex.Unlock()
}
// OnShutdown is a hook to execute user functions after Shutdown.
func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) {
// OnPreShutdown is a hook to execute user functions before Shutdown.
func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) {
h.app.mutex.Lock()
h.onShutdown = append(h.onShutdown, handler...)
h.onPreShutdown = append(h.onPreShutdown, handler...)
h.app.mutex.Unlock()
}
// OnPostShutdown is a hook to execute user functions after Shutdown.
func (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler) {
h.app.mutex.Lock()
h.onPostShutdown = append(h.onPostShutdown, handler...)
h.app.mutex.Unlock()
}
@ -191,10 +201,18 @@ func (h *Hooks) executeOnListenHooks(listenData ListenData) error {
return nil
}
func (h *Hooks) executeOnShutdownHooks() {
for _, v := range h.onShutdown {
func (h *Hooks) executeOnPreShutdownHooks() {
for _, v := range h.onPreShutdown {
if err := v(); err != nil {
log.Errorf("failed to call shutdown hook: %v", err)
log.Errorf("failed to call pre shutdown hook: %v", err)
}
}
}
func (h *Hooks) executeOnPostShutdownHooks(err error) {
for _, v := range h.onPostShutdown {
if err := v(err); err != nil {
log.Errorf("failed to call post shutdown hook: %v", err)
}
}
}

View File

@ -2,7 +2,6 @@ package fiber
import (
"errors"
"fmt"
"testing"
"time"
@ -83,17 +82,14 @@ func Test_Hook_OnName(t *testing.T) {
func Test_Hook_OnName_Error(t *testing.T) {
t.Parallel()
app := New()
defer func() {
if err := recover(); err != nil {
require.Equal(t, "unknown error", fmt.Sprintf("%v", err))
}
}()
app.Hooks().OnName(func(_ Route) error {
return errors.New("unknown error")
})
app.Get("/", testSimpleHandler).Name("index")
require.PanicsWithError(t, "unknown error", func() {
app.Get("/", testSimpleHandler).Name("index")
})
}
func Test_Hook_OnGroup(t *testing.T) {
@ -167,36 +163,99 @@ func Test_Hook_OnGroupName(t *testing.T) {
func Test_Hook_OnGroupName_Error(t *testing.T) {
t.Parallel()
app := New()
defer func() {
if err := recover(); err != nil {
require.Equal(t, "unknown error", fmt.Sprintf("%v", err))
}
}()
app.Hooks().OnGroupName(func(_ Group) error {
return errors.New("unknown error")
})
grp := app.Group("/x").Name("x.")
grp.Get("/test", testSimpleHandler)
require.PanicsWithError(t, "unknown error", func() {
_ = app.Group("/x").Name("x.")
})
}
func Test_Hook_OnShutdown(t *testing.T) {
func Test_Hook_OnPrehutdown(t *testing.T) {
t.Parallel()
app := New()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app.Hooks().OnShutdown(func() error {
_, err := buf.WriteString("shutdowning")
app.Hooks().OnPreShutdown(func() error {
_, err := buf.WriteString("pre-shutdowning")
require.NoError(t, err)
return nil
})
require.NoError(t, app.Shutdown())
require.Equal(t, "shutdowning", buf.String())
require.Equal(t, "pre-shutdowning", buf.String())
}
func Test_Hook_OnPostShutdown(t *testing.T) {
t.Run("should execute post shutdown hook with error", func(t *testing.T) {
app := New()
expectedErr := errors.New("test shutdown error")
hookCalled := make(chan error, 1)
defer close(hookCalled)
app.Hooks().OnPostShutdown(func(err error) error {
hookCalled <- err
return nil
})
go func() {
if err := app.Listen(":0"); err != nil {
return
}
}()
time.Sleep(100 * time.Millisecond)
app.hooks.executeOnPostShutdownHooks(expectedErr)
select {
case err := <-hookCalled:
require.Equal(t, expectedErr, err)
case <-time.After(time.Second):
t.Fatal("hook execution timeout")
}
require.NoError(t, app.Shutdown())
})
t.Run("should execute multiple hooks in order", func(t *testing.T) {
app := New()
execution := make([]int, 0)
app.Hooks().OnPostShutdown(func(_ error) error {
execution = append(execution, 1)
return nil
})
app.Hooks().OnPostShutdown(func(_ error) error {
execution = append(execution, 2)
return nil
})
app.hooks.executeOnPostShutdownHooks(nil)
require.Len(t, execution, 2, "expected 2 hooks to execute")
require.Equal(t, []int{1, 2}, execution, "hooks executed in wrong order")
})
t.Run("should handle hook error", func(_ *testing.T) {
app := New()
hookErr := errors.New("hook error")
app.Hooks().OnPostShutdown(func(_ error) error {
return hookErr
})
// Should not panic
app.hooks.executeOnPostShutdownHooks(nil)
})
}
func Test_Hook_OnListen(t *testing.T) {

View File

@ -60,17 +60,6 @@ type ListenConfig struct {
// Default: nil
BeforeServeFunc func(app *App) error `json:"before_serve_func"`
// OnShutdownError allows to customize error behavior when to graceful shutdown server by given signal.
//
// Print error with log.Fatalf() by default.
// Default: nil
OnShutdownError func(err error)
// OnShutdownSuccess allows to customize success behavior when to graceful shutdown server by given signal.
//
// Default: nil
OnShutdownSuccess func()
// AutoCertManager manages TLS certificates automatically using the ACME protocol,
// Enables integration with Let's Encrypt or other ACME-compatible providers.
//
@ -102,7 +91,7 @@ type ListenConfig struct {
CertClientFile string `json:"cert_client_file"`
// When the graceful shutdown begins, use this field to set the timeout
// duration. If the timeout is reached, OnShutdownError will be called.
// duration. If the timeout is reached, OnPostShutdown will be called with the error.
// Set to 0 to disable the timeout and wait indefinitely.
//
// Default: 10 * time.Second
@ -136,9 +125,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig {
return ListenConfig{
TLSMinVersion: tls.VersionTLS12,
ListenerNetwork: NetworkTCP4,
OnShutdownError: func(err error) {
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option
},
ShutdownTimeout: 10 * time.Second,
}
}
@ -148,12 +134,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig {
cfg.ListenerNetwork = NetworkTCP4
}
if cfg.OnShutdownError == nil {
cfg.OnShutdownError = func(err error) {
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option
}
}
if cfg.TLSMinVersion == 0 {
cfg.TLSMinVersion = tls.VersionTLS12
}
@ -517,11 +497,9 @@ func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) {
}
if err != nil {
cfg.OnShutdownError(err)
app.hooks.executeOnPostShutdownHooks(err)
return
}
if success := cfg.OnShutdownSuccess; success != nil {
success()
}
app.hooks.executeOnPostShutdownHooks(nil)
}

View File

@ -15,6 +15,7 @@ import (
"testing"
"time"
"github.com/gofiber/utils/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
@ -37,98 +38,42 @@ func Test_Listen(t *testing.T) {
// go test -run Test_Listen_Graceful_Shutdown
func Test_Listen_Graceful_Shutdown(t *testing.T) {
var mu sync.Mutex
var shutdown bool
app := New()
app.Get("/", func(c Ctx) error {
return c.SendString(c.Hostname())
t.Run("Basic Graceful Shutdown", func(t *testing.T) {
testGracefulShutdown(t, 0)
})
ln := fasthttputil.NewInmemoryListener()
errs := make(chan error)
t.Run("Shutdown With Timeout", func(t *testing.T) {
testGracefulShutdown(t, 500*time.Millisecond)
})
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
errs <- app.Listener(ln, ListenConfig{
DisableStartupMessage: true,
GracefulContext: ctx,
OnShutdownSuccess: func() {
mu.Lock()
shutdown = true
mu.Unlock()
},
})
}()
// Server readiness check
for i := 0; i < 10; i++ {
conn, err := ln.Dial()
if err == nil {
conn.Close() //nolint:errcheck // ignore error
break
}
// Wait a bit before retrying
time.Sleep(100 * time.Millisecond)
if i == 9 {
t.Fatalf("Server did not become ready in time: %v", err)
}
}
testCases := []struct {
ExpectedErr error
ExpectedBody string
Time time.Duration
ExpectedStatusCode int
}{
{Time: 500 * time.Millisecond, ExpectedBody: "example.com", ExpectedStatusCode: StatusOK, ExpectedErr: nil},
{Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: fasthttputil.ErrInmemoryListenerClosed},
}
for _, tc := range testCases {
time.Sleep(tc.Time)
req := fasthttp.AcquireRequest()
req.SetRequestURI("http://example.com")
client := fasthttp.HostClient{}
client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() }
resp := fasthttp.AcquireResponse()
err := client.Do(req, resp)
require.Equal(t, tc.ExpectedErr, err)
require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode())
require.Equal(t, tc.ExpectedBody, string(resp.Body()))
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(resp)
}
mu.Lock()
err := <-errs
require.True(t, shutdown)
require.NoError(t, err)
mu.Unlock()
t.Run("Shutdown With Timeout Error", func(t *testing.T) {
testGracefulShutdown(t, 1*time.Nanosecond)
})
}
// go test -run Test_Listen_Graceful_Shutdown_Timeout
func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) {
func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) {
t.Helper()
var mu sync.Mutex
var shutdownSuccess bool
var shutdownTimeoutError error
var shutdown bool
var receivedErr error
app := New()
app.Get("/", func(c Ctx) error {
time.Sleep(10 * time.Millisecond)
return c.SendString(c.Hostname())
})
ln := fasthttputil.NewInmemoryListener()
errs := make(chan error)
errs := make(chan error, 1)
app.hooks.OnPostShutdown(func(err error) error {
mu.Lock()
defer mu.Unlock()
shutdown = true
receivedErr = err
return nil
})
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
@ -137,93 +82,83 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) {
errs <- app.Listener(ln, ListenConfig{
DisableStartupMessage: true,
GracefulContext: ctx,
ShutdownTimeout: 500 * time.Millisecond,
OnShutdownSuccess: func() {
mu.Lock()
shutdownSuccess = true
mu.Unlock()
},
OnShutdownError: func(err error) {
mu.Lock()
shutdownTimeoutError = err
mu.Unlock()
},
ShutdownTimeout: shutdownTimeout,
})
}()
// Server readiness check
for i := 0; i < 10; i++ {
require.Eventually(t, func() bool {
conn, err := ln.Dial()
// To test a graceful shutdown timeout, do not close the connection.
if err == nil {
_ = conn
break
}
// Wait a bit before retrying
time.Sleep(100 * time.Millisecond)
if i == 9 {
t.Fatalf("Server did not become ready in time: %v", err)
if err := conn.Close(); err != nil {
t.Logf("error closing connection: %v", err)
}
return true
}
return false
}, time.Second, 100*time.Millisecond, "Server failed to become ready")
client := fasthttp.HostClient{
Dial: func(_ string) (net.Conn, error) { return ln.Dial() },
}
testCases := []struct {
ExpectedErr error
ExpectedShutdownError error
ExpectedBody string
Time time.Duration
ExpectedStatusCode int
ExpectedShutdownSuccess bool
}{
type testCase struct {
expectedErr error
expectedBody string
name string
waitTime time.Duration
expectedStatusCode int
closeConnection bool
}
testCases := []testCase{
{
Time: 100 * time.Millisecond,
ExpectedBody: "example.com",
ExpectedStatusCode: StatusOK,
ExpectedErr: nil,
ExpectedShutdownError: nil,
ExpectedShutdownSuccess: false,
name: "Server running normally",
waitTime: 500 * time.Millisecond,
expectedBody: "example.com",
expectedStatusCode: StatusOK,
expectedErr: nil,
closeConnection: true,
},
{
Time: 3 * time.Second,
ExpectedBody: "",
ExpectedStatusCode: StatusOK,
ExpectedErr: fasthttputil.ErrInmemoryListenerClosed,
ExpectedShutdownError: context.DeadlineExceeded,
ExpectedShutdownSuccess: false,
name: "Server shutdown complete",
waitTime: 3 * time.Second,
expectedBody: "",
expectedStatusCode: StatusOK,
expectedErr: fasthttputil.ErrInmemoryListenerClosed,
closeConnection: true,
},
}
for _, tc := range testCases {
time.Sleep(tc.Time)
t.Run(tc.name, func(t *testing.T) {
time.Sleep(tc.waitTime)
req := fasthttp.AcquireRequest()
req.SetRequestURI("http://example.com")
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetRequestURI("http://example.com")
client := fasthttp.HostClient{}
client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() }
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
resp := fasthttp.AcquireResponse()
err := client.Do(req, resp)
err := client.Do(req, resp)
if err == nil {
require.NoError(t, err)
require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode())
require.Equal(t, tc.ExpectedBody, string(resp.Body()))
} else {
require.ErrorIs(t, err, tc.ExpectedErr)
}
mu.Lock()
require.Equal(t, tc.ExpectedShutdownSuccess, shutdownSuccess)
require.Equal(t, tc.ExpectedShutdownError, shutdownTimeoutError)
mu.Unlock()
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(resp)
if tc.expectedErr == nil {
require.NoError(t, err)
require.Equal(t, tc.expectedStatusCode, resp.StatusCode())
require.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body()))
} else {
require.ErrorIs(t, err, tc.expectedErr)
}
})
}
mu.Lock()
err := <-errs
require.NoError(t, err)
require.True(t, shutdown)
if shutdownTimeout == 1*time.Nanosecond {
require.Error(t, receivedErr)
require.ErrorIs(t, receivedErr, context.DeadlineExceeded)
}
require.NoError(t, <-errs)
mu.Unlock()
}

View File

@ -35,17 +35,32 @@ const (
noStore = "no-store"
)
var ignoreHeaders = map[string]any{
"Connection": nil,
"Keep-Alive": nil,
"Proxy-Authenticate": nil,
"Proxy-Authorization": nil,
"TE": nil,
"Trailers": nil,
"Transfer-Encoding": nil,
"Upgrade": nil,
"Content-Type": nil, // already stored explicitly by the cache manager
"Content-Encoding": nil, // already stored explicitly by the cache manager
var ignoreHeaders = map[string]struct{}{
"Connection": {},
"Keep-Alive": {},
"Proxy-Authenticate": {},
"Proxy-Authorization": {},
"TE": {},
"Trailers": {},
"Transfer-Encoding": {},
"Upgrade": {},
"Content-Type": {}, // already stored explicitly by the cache manager
"Content-Encoding": {}, // already stored explicitly by the cache manager
}
var cacheableStatusCodes = map[int]bool{
fiber.StatusOK: true,
fiber.StatusNonAuthoritativeInformation: true,
fiber.StatusNoContent: true,
fiber.StatusPartialContent: true,
fiber.StatusMultipleChoices: true,
fiber.StatusMovedPermanently: true,
fiber.StatusNotFound: true,
fiber.StatusMethodNotAllowed: true,
fiber.StatusGone: true,
fiber.StatusRequestURITooLong: true,
fiber.StatusTeapot: true,
fiber.StatusNotImplemented: true,
}
// New creates a new middleware handler
@ -170,6 +185,12 @@ func New(config ...Config) fiber.Handler {
return err
}
// Don't cache response if status code is not cacheable
if !cacheableStatusCodes[c.Response().StatusCode()] {
c.Set(cfg.CacheHeader, cacheUnreachable)
return nil
}
// lock entry back and unlock on finish
mux.Lock()
defer mux.Unlock()

View File

@ -918,6 +918,87 @@ func Test_Cache_MaxBytesSizes(t *testing.T) {
}
}
func Test_Cache_UncacheableStatusCodes(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New())
app.Get("/:statusCode", func(c fiber.Ctx) error {
statusCode, err := strconv.Atoi(c.Params("statusCode"))
require.NoError(t, err)
return c.Status(statusCode).SendString("foo")
})
uncacheableStatusCodes := []int{
// Informational responses
fiber.StatusContinue,
fiber.StatusSwitchingProtocols,
fiber.StatusProcessing,
fiber.StatusEarlyHints,
// Successful responses
fiber.StatusCreated,
fiber.StatusAccepted,
fiber.StatusResetContent,
fiber.StatusMultiStatus,
fiber.StatusAlreadyReported,
fiber.StatusIMUsed,
// Redirection responses
fiber.StatusFound,
fiber.StatusSeeOther,
fiber.StatusNotModified,
fiber.StatusUseProxy,
fiber.StatusSwitchProxy,
fiber.StatusTemporaryRedirect,
fiber.StatusPermanentRedirect,
// Client error responses
fiber.StatusBadRequest,
fiber.StatusUnauthorized,
fiber.StatusPaymentRequired,
fiber.StatusForbidden,
fiber.StatusNotAcceptable,
fiber.StatusProxyAuthRequired,
fiber.StatusRequestTimeout,
fiber.StatusConflict,
fiber.StatusLengthRequired,
fiber.StatusPreconditionFailed,
fiber.StatusRequestEntityTooLarge,
fiber.StatusUnsupportedMediaType,
fiber.StatusRequestedRangeNotSatisfiable,
fiber.StatusExpectationFailed,
fiber.StatusMisdirectedRequest,
fiber.StatusUnprocessableEntity,
fiber.StatusLocked,
fiber.StatusFailedDependency,
fiber.StatusTooEarly,
fiber.StatusUpgradeRequired,
fiber.StatusPreconditionRequired,
fiber.StatusTooManyRequests,
fiber.StatusRequestHeaderFieldsTooLarge,
fiber.StatusUnavailableForLegalReasons,
// Server error responses
fiber.StatusInternalServerError,
fiber.StatusBadGateway,
fiber.StatusServiceUnavailable,
fiber.StatusGatewayTimeout,
fiber.StatusHTTPVersionNotSupported,
fiber.StatusVariantAlsoNegotiates,
fiber.StatusInsufficientStorage,
fiber.StatusLoopDetected,
fiber.StatusNotExtended,
fiber.StatusNetworkAuthenticationRequired,
}
for _, v := range uncacheableStatusCodes {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf("/%d", v), nil))
require.NoError(t, err)
require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache"))
require.Equal(t, v, resp.StatusCode)
}
}
// go test -v -run=^$ -bench=Benchmark_Cache -benchmem -count=4
func Benchmark_Cache(b *testing.B) {
app := fiber.New()

View File

@ -10,42 +10,58 @@ type Locker interface {
Unlock(key string) error
}
type countedLock struct {
mu sync.Mutex
locked int
}
type MemoryLock struct {
keys map[string]*sync.Mutex
keys map[string]*countedLock
mu sync.Mutex
}
func (l *MemoryLock) Lock(key string) error {
l.mu.Lock()
mu, ok := l.keys[key]
lock, ok := l.keys[key]
if !ok {
mu = new(sync.Mutex)
l.keys[key] = mu
lock = new(countedLock)
l.keys[key] = lock
}
lock.locked++
l.mu.Unlock()
mu.Lock()
lock.mu.Lock()
return nil
}
func (l *MemoryLock) Unlock(key string) error {
l.mu.Lock()
mu, ok := l.keys[key]
l.mu.Unlock()
lock, ok := l.keys[key]
if !ok {
// This happens if we try to unlock an unknown key
l.mu.Unlock()
return nil
}
l.mu.Unlock()
mu.Unlock()
lock.mu.Unlock()
l.mu.Lock()
lock.locked--
if lock.locked <= 0 {
// This happens if countedLock is used to Lock and Unlock the same number of times
// So, we can delete the key to prevent memory leak
delete(l.keys, key)
}
l.mu.Unlock()
return nil
}
func NewMemoryLock() *MemoryLock {
return &MemoryLock{
keys: make(map[string]*sync.Mutex),
keys: make(map[string]*countedLock),
}
}

View File

@ -1,6 +1,8 @@
package idempotency_test
import (
"strconv"
"sync/atomic"
"testing"
"time"
@ -59,3 +61,67 @@ func Test_MemoryLock(t *testing.T) {
require.NoError(t, err)
}
}
func Benchmark_MemoryLock(b *testing.B) {
keys := make([]string, b.N)
for i := range keys {
keys[i] = strconv.Itoa(i)
}
lock := idempotency.NewMemoryLock()
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
}
func Benchmark_MemoryLock_Parallel(b *testing.B) {
// In order to prevent using repeated keys I pre-allocate keys
keys := make([]string, 1_000_000)
for i := range keys {
keys[i] = strconv.Itoa(i)
}
b.Run("UniqueKeys", func(b *testing.B) {
lock := idempotency.NewMemoryLock()
var keyI atomic.Int32
b.RunParallel(func(p *testing.PB) {
for p.Next() {
i := int(keyI.Add(1)) % len(keys)
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
})
})
b.Run("RepeatedKeys", func(b *testing.B) {
lock := idempotency.NewMemoryLock()
var keyI atomic.Int32
b.RunParallel(func(p *testing.PB) {
for p.Next() {
// Division by 3 ensures that index will be repreated exactly 3 times
i := int(keyI.Add(1)) / 3 % len(keys)
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
})
})
}

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.
//
@ -98,12 +103,13 @@ 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,
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Output: os.Stdout,
Stream: os.Stdout,
BeforeHandlerFunc: beforeHandlerFunc,
LoggerFunc: defaultLoggerInstance,
enableColors: true,
@ -126,6 +132,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 +150,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 +163,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
@ -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)
}
}
}

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
@ -358,7 +499,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 +544,7 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) {
app := fiber.New()
logger := New(Config{
Output: buff,
Stream: buff,
})
app.Use(logger)
@ -453,7 +594,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 +615,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 +649,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 +677,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 +752,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 +775,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 +799,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 +830,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 +854,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 {
@ -759,7 +900,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 +923,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 +935,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 +946,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 +960,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 +972,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 +985,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 +998,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 +1015,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 +1027,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 +1039,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")
@ -927,7 +1068,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 +1091,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 +1103,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 +1116,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 +1127,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 +1140,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 +1153,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 +1166,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 +1183,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 +1195,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 +1207,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")
@ -1095,7 +1236,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

@ -66,6 +66,7 @@ func New(root string, cfg ...Config) fiber.Handler {
AcceptByteRange: config.ByteRange,
Compress: config.Compress,
CompressBrotli: config.Compress, // Brotli compression won't work without this
CompressZstd: config.Compress, // Zstd compression won't work without this
CompressedFileSuffixes: c.App().Config().CompressedFileSuffixes,
CacheDuration: config.CacheDuration,
SkipCache: config.CacheDuration < 0,

View File

@ -8,23 +8,52 @@ import (
"github.com/gofiber/fiber/v3"
)
// New implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response.
func New(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler {
// New enforces a timeout for each incoming request. If the timeout expires or
// any of the specified errors occur, fiber.ErrRequestTimeout is returned.
func New(h fiber.Handler, timeout time.Duration, tErrs ...error) fiber.Handler {
return func(ctx fiber.Ctx) error {
timeoutContext, cancel := context.WithTimeout(ctx.Context(), t)
defer cancel()
ctx.SetContext(timeoutContext)
if err := h(ctx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return fiber.ErrRequestTimeout
}
for i := range tErrs {
if errors.Is(err, tErrs[i]) {
return fiber.ErrRequestTimeout
}
}
return err
// If timeout <= 0, skip context.WithTimeout and run the handler as-is.
if timeout <= 0 {
return runHandler(ctx, h, tErrs)
}
return nil
// Create a context with the specified timeout; any operation exceeding
// this deadline will be canceled automatically.
timeoutContext, cancel := context.WithTimeout(ctx.Context(), timeout)
defer cancel()
// Replace the default Fiber context with our timeout-bound context.
ctx.SetContext(timeoutContext)
// Run the handler and check for relevant errors.
err := runHandler(ctx, h, tErrs)
// If the context actually timed out, return a timeout error.
if errors.Is(timeoutContext.Err(), context.DeadlineExceeded) {
return fiber.ErrRequestTimeout
}
return err
}
}
// runHandler executes the handler and returns fiber.ErrRequestTimeout if it
// sees a deadline exceeded error or one of the custom "timeout-like" errors.
func runHandler(c fiber.Ctx, h fiber.Handler, tErrs []error) error {
// Execute the wrapped handler synchronously.
err := h(c)
// If the context has timed out, return a request timeout error.
if err != nil && (errors.Is(err, context.DeadlineExceeded) || isCustomError(err, tErrs)) {
return fiber.ErrRequestTimeout
}
return err
}
// isCustomError checks whether err matches any error in errList using errors.Is.
func isCustomError(err error, errList []error) bool {
for _, e := range errList {
if errors.Is(err, e) {
return true
}
}
return false
}

View File

@ -12,77 +12,119 @@ import (
"github.com/stretchr/testify/require"
)
// go test -run Test_WithContextTimeout
func Test_WithContextTimeout(t *testing.T) {
t.Parallel()
// fiber instance
app := fiber.New()
h := New(func(c fiber.Ctx) error {
sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms")
require.NoError(t, err)
if err := sleepWithContext(c.Context(), sleepTime, context.DeadlineExceeded); err != nil {
return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err))
}
return nil
}, 100*time.Millisecond)
app.Get("/test/:sleepTime", h)
testTimeout := func(timeoutStr string) {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code")
}
testSucces := func(timeoutStr string) {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code")
}
testTimeout("300")
testTimeout("500")
testSucces("50")
testSucces("30")
}
var (
// Custom error that we treat like a timeout when returned by the handler.
errCustomTimeout = errors.New("custom timeout error")
var ErrFooTimeOut = errors.New("foo context canceled")
// go test -run Test_WithContextTimeoutWithCustomError
func Test_WithContextTimeoutWithCustomError(t *testing.T) {
t.Parallel()
// fiber instance
app := fiber.New()
h := New(func(c fiber.Ctx) error {
sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms")
require.NoError(t, err)
if err := sleepWithContext(c.Context(), sleepTime, ErrFooTimeOut); err != nil {
return fmt.Errorf("%w: execution error", err)
}
return nil
}, 100*time.Millisecond, ErrFooTimeOut)
app.Get("/test/:sleepTime", h)
testTimeout := func(timeoutStr string) {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code")
}
testSucces := func(timeoutStr string) {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Status code")
}
testTimeout("300")
testTimeout("500")
testSucces("50")
testSucces("30")
}
// Some unrelated error that should NOT trigger a request timeout.
errUnrelated = errors.New("unmatched error")
)
// sleepWithContext simulates a task that takes `d` time, but returns `te` if the context is canceled.
func sleepWithContext(ctx context.Context, d time.Duration, te error) error {
timer := time.NewTimer(d)
defer timer.Stop() // Clean up the timer
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return te
case <-timer.C:
return nil
}
return nil
}
// TestTimeout_Success tests a handler that completes within the allotted timeout.
func TestTimeout_Success(t *testing.T) {
t.Parallel()
app := fiber.New()
// Our middleware wraps a handler that sleeps for 10ms, well under the 50ms limit.
app.Get("/fast", New(func(c fiber.Ctx) error {
// Simulate some work
if err := sleepWithContext(c.Context(), 10*time.Millisecond, context.DeadlineExceeded); err != nil {
return err
}
return c.SendString("OK")
}, 50*time.Millisecond))
req := httptest.NewRequest(fiber.MethodGet, "/fast", nil)
resp, err := app.Test(req)
require.NoError(t, err, "app.Test(req) should not fail")
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Expected 200 OK for fast requests")
}
// TestTimeout_Exceeded tests a handler that exceeds the provided timeout.
func TestTimeout_Exceeded(t *testing.T) {
t.Parallel()
app := fiber.New()
// This handler sleeps 200ms, exceeding the 100ms limit.
app.Get("/slow", New(func(c fiber.Ctx) error {
if err := sleepWithContext(c.Context(), 200*time.Millisecond, context.DeadlineExceeded); err != nil {
return err
}
return c.SendString("Should never get here")
}, 100*time.Millisecond))
req := httptest.NewRequest(fiber.MethodGet, "/slow", nil)
resp, err := app.Test(req)
require.NoError(t, err, "app.Test(req) should not fail")
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Expected 408 Request Timeout")
}
// TestTimeout_CustomError tests that returning a user-defined error is also treated as a timeout.
func TestTimeout_CustomError(t *testing.T) {
t.Parallel()
app := fiber.New()
// This handler sleeps 50ms and returns errCustomTimeout if canceled.
app.Get("/custom", New(func(c fiber.Ctx) error {
// Sleep might time out, or might return early. If the context is canceled,
// we treat errCustomTimeout as a 'timeout-like' condition.
if err := sleepWithContext(c.Context(), 200*time.Millisecond, errCustomTimeout); err != nil {
return fmt.Errorf("wrapped: %w", err)
}
return c.SendString("Should never get here")
}, 100*time.Millisecond, errCustomTimeout))
req := httptest.NewRequest(fiber.MethodGet, "/custom", nil)
resp, err := app.Test(req)
require.NoError(t, err, "app.Test(req) should not fail")
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Expected 408 for custom timeout error")
}
// TestTimeout_UnmatchedError checks that if the handler returns an error
// that is neither a deadline exceeded nor a custom 'timeout' error, it is
// propagated as a regular 500 (internal server error).
func TestTimeout_UnmatchedError(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Get("/unmatched", New(func(_ fiber.Ctx) error {
return errUnrelated // Not in the custom error list
}, 100*time.Millisecond, errCustomTimeout))
req := httptest.NewRequest(fiber.MethodGet, "/unmatched", nil)
resp, err := app.Test(req)
require.NoError(t, err, "app.Test(req) should not fail")
require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode,
"Expected 500 because the error is not recognized as a timeout error")
}
// TestTimeout_ZeroDuration tests the edge case where the timeout is set to zero.
// Usually this means the request can never exceed a 'deadline' effectively no timeout.
func TestTimeout_ZeroDuration(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Get("/zero", New(func(c fiber.Ctx) error {
// Sleep 50ms, but there's no real 'deadline' since zero-timeout.
time.Sleep(50 * time.Millisecond)
return c.SendString("No timeout used")
}, 0))
req := httptest.NewRequest(fiber.MethodGet, "/zero", nil)
resp, err := app.Test(req)
require.NoError(t, err, "app.Test(req) should not fail")
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Expected 200 OK with zero timeout")
}

View File

@ -55,7 +55,7 @@ func (app *App) mount(prefix string, subApp *App) Router {
// register mounted group
mountGroup := &Group{Prefix: prefix, app: subApp}
app.register([]string{methodUse}, prefix, mountGroup, nil)
app.register([]string{methodUse}, prefix, mountGroup)
// Execute onMount hooks
if err := subApp.hooks.executeOnMountHooks(app); err != nil {
@ -85,7 +85,7 @@ func (grp *Group) mount(prefix string, subApp *App) Router {
// register mounted group
mountGroup := &Group{Prefix: groupPath, app: subApp}
grp.app.register([]string{methodUse}, groupPath, mountGroup, nil)
grp.app.register([]string{methodUse}, groupPath, mountGroup)
// Execute onMount hooks
if err := subApp.hooks.executeOnMountHooks(grp.app); err != nil {

141
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,12 +611,30 @@ func GetTrimmedParam(param string) string {
return param[start:end]
}
// RemoveEscapeChar remove escape characters
// RemoveEscapeChar removes escape characters
func RemoveEscapeChar(word string) string {
if strings.IndexByte(word, escapeChar) != -1 {
return strings.ReplaceAll(word, string(escapeChar), "")
b := []byte(word)
dst := 0
for src := 0; src < len(b); src++ {
if b[src] == '\\' {
continue
}
b[dst] = b[src]
dst++
}
return word
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 {

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

@ -6,18 +6,18 @@ package fiber
// Register defines all router handle interface generate by Route().
type Register interface {
All(handler Handler, middleware ...Handler) Register
Get(handler Handler, middleware ...Handler) Register
Head(handler Handler, middleware ...Handler) Register
Post(handler Handler, middleware ...Handler) Register
Put(handler Handler, middleware ...Handler) Register
Delete(handler Handler, middleware ...Handler) Register
Connect(handler Handler, middleware ...Handler) Register
Options(handler Handler, middleware ...Handler) Register
Trace(handler Handler, middleware ...Handler) Register
Patch(handler Handler, middleware ...Handler) Register
All(handler Handler, handlers ...Handler) Register
Get(handler Handler, handlers ...Handler) Register
Head(handler Handler, handlers ...Handler) Register
Post(handler Handler, handlers ...Handler) Register
Put(handler Handler, handlers ...Handler) Register
Delete(handler Handler, handlers ...Handler) Register
Connect(handler Handler, handlers ...Handler) Register
Options(handler Handler, handlers ...Handler) Register
Trace(handler Handler, handlers ...Handler) Register
Patch(handler Handler, handlers ...Handler) Register
Add(methods []string, handler Handler, middleware ...Handler) Register
Add(methods []string, handler Handler, handlers ...Handler) Register
Route(path string) Register
}
@ -45,68 +45,68 @@ type Registering struct {
// })
//
// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc...
func (r *Registering) All(handler Handler, middleware ...Handler) Register {
r.app.register([]string{methodUse}, r.path, nil, handler, middleware...)
func (r *Registering) All(handler Handler, handlers ...Handler) Register {
r.app.register([]string{methodUse}, r.path, nil, append([]Handler{handler}, handlers...)...)
return r
}
// Get registers a route for GET methods that requests a representation
// of the specified resource. Requests using GET should only retrieve data.
func (r *Registering) Get(handler Handler, middleware ...Handler) Register {
r.app.Add([]string{MethodGet}, r.path, handler, middleware...)
func (r *Registering) Get(handler Handler, handlers ...Handler) Register {
r.app.Add([]string{MethodGet}, r.path, handler, handlers...)
return r
}
// Head registers a route for HEAD methods that asks for a response identical
// to that of a GET request, but without the response body.
func (r *Registering) Head(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodHead}, handler, middleware...)
func (r *Registering) Head(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodHead}, handler, handlers...)
}
// Post registers a route for POST methods that is used to submit an entity to the
// specified resource, often causing a change in state or side effects on the server.
func (r *Registering) Post(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodPost}, handler, middleware...)
func (r *Registering) Post(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodPost}, handler, handlers...)
}
// Put registers a route for PUT methods that replaces all current representations
// of the target resource with the request payload.
func (r *Registering) Put(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodPut}, handler, middleware...)
func (r *Registering) Put(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodPut}, handler, handlers...)
}
// Delete registers a route for DELETE methods that deletes the specified resource.
func (r *Registering) Delete(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodDelete}, handler, middleware...)
func (r *Registering) Delete(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodDelete}, handler, handlers...)
}
// Connect registers a route for CONNECT methods that establishes a tunnel to the
// server identified by the target resource.
func (r *Registering) Connect(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodConnect}, handler, middleware...)
func (r *Registering) Connect(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodConnect}, handler, handlers...)
}
// Options registers a route for OPTIONS methods that is used to describe the
// communication options for the target resource.
func (r *Registering) Options(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodOptions}, handler, middleware...)
func (r *Registering) Options(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodOptions}, handler, handlers...)
}
// Trace registers a route for TRACE methods that performs a message loop-back
// test along the r.Path to the target resource.
func (r *Registering) Trace(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodTrace}, handler, middleware...)
func (r *Registering) Trace(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodTrace}, handler, handlers...)
}
// Patch registers a route for PATCH methods that is used to apply partial
// modifications to a resource.
func (r *Registering) Patch(handler Handler, middleware ...Handler) Register {
return r.Add([]string{MethodPatch}, handler, middleware...)
func (r *Registering) Patch(handler Handler, handlers ...Handler) Register {
return r.Add([]string{MethodPatch}, handler, handlers...)
}
// Add allows you to specify multiple HTTP methods to register a route.
func (r *Registering) Add(methods []string, handler Handler, middleware ...Handler) Register {
r.app.register(methods, r.path, nil, handler, middleware...)
func (r *Registering) Add(methods []string, handler Handler, handlers ...Handler) Register {
r.app.register(methods, r.path, nil, append([]Handler{handler}, handlers...)...)
return r
}

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
}

233
router.go
View File

@ -5,12 +5,12 @@
package fiber
import (
"bytes"
"errors"
"fmt"
"html"
"slices"
"sort"
"strings"
"sync/atomic"
"github.com/gofiber/utils/v2"
@ -21,18 +21,18 @@ import (
type Router interface {
Use(args ...any) Router
Get(path string, handler Handler, middleware ...Handler) Router
Head(path string, handler Handler, middleware ...Handler) Router
Post(path string, handler Handler, middleware ...Handler) Router
Put(path string, handler Handler, middleware ...Handler) Router
Delete(path string, handler Handler, middleware ...Handler) Router
Connect(path string, handler Handler, middleware ...Handler) Router
Options(path string, handler Handler, middleware ...Handler) Router
Trace(path string, handler Handler, middleware ...Handler) Router
Patch(path string, handler Handler, middleware ...Handler) Router
Get(path string, handler Handler, handlers ...Handler) Router
Head(path string, handler Handler, handlers ...Handler) Router
Post(path string, handler Handler, handlers ...Handler) Router
Put(path string, handler Handler, handlers ...Handler) Router
Delete(path string, handler Handler, handlers ...Handler) Router
Connect(path string, handler Handler, handlers ...Handler) Router
Options(path string, handler Handler, handlers ...Handler) Router
Trace(path string, handler Handler, handlers ...Handler) Router
Patch(path string, handler Handler, handlers ...Handler) Router
Add(methods []string, path string, handler Handler, middleware ...Handler) Router
All(path string, handler Handler, middleware ...Handler) Router
Add(methods []string, path string, handler Handler, handlers ...Handler) Router
All(path string, handler Handler, handlers ...Handler) Router
Group(prefix string, handlers ...Handler) Router
@ -66,10 +66,12 @@ type Route struct {
func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
// root detectionPath check
if r.root && detectionPath == "/" {
if r.root && len(detectionPath) == 1 && detectionPath[0] == '/' {
return true
// '*' wildcard matches any detectionPath
} else if r.star {
}
// '*' wildcard matches any detectionPath
if r.star {
if len(path) > 1 {
params[0] = path[1:]
} else {
@ -77,24 +79,32 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo
}
return true
}
// Does this route have parameters
// Does this route have parameters?
if len(r.Params) > 0 {
// Match params
if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match {
// Get params from the path detectionPath
return match
}
}
// Is this route a Middleware?
if r.use {
// Single slash will match or detectionPath prefix
if r.root || strings.HasPrefix(detectionPath, r.path) {
// Match params using precomputed routeParser
if r.routeParser.getMatch(detectionPath, path, params, r.use) {
return true
}
// Check for a simple detectionPath match
} else if len(r.path) == len(detectionPath) && r.path == detectionPath {
}
// Middleware route?
if r.use {
// Single slash or prefix match
plen := len(r.path)
if r.root {
// If r.root is '/', it matches everything starting at '/'
if len(detectionPath) > 0 && detectionPath[0] == '/' {
return true
}
} else if len(detectionPath) >= plen && detectionPath[:plen] == r.path {
return true
}
} else if len(r.path) == len(detectionPath) && detectionPath == r.path {
// Check exact match
return true
}
// No match
return false
}
@ -202,44 +212,63 @@ func (app *App) next(c *DefaultCtx) (bool, error) {
return false, err
}
func (app *App) requestHandler(rctx *fasthttp.RequestCtx) {
// Handler for default ctxs
var c CustomCtx
var ok bool
if app.newCtxFunc != nil {
c, ok = app.AcquireCtx(rctx).(CustomCtx)
if !ok {
panic(errors.New("requestHandler: failed to type-assert to CustomCtx"))
}
} else {
c, ok = app.AcquireCtx(rctx).(*DefaultCtx)
if !ok {
panic(errors.New("requestHandler: failed to type-assert to *DefaultCtx"))
}
func (app *App) defaultRequestHandler(rctx *fasthttp.RequestCtx) {
// Acquire DefaultCtx from the pool
ctx, ok := app.AcquireCtx(rctx).(*DefaultCtx)
if !ok {
panic(errors.New("requestHandler: failed to type-assert to *DefaultCtx"))
}
defer app.ReleaseCtx(c)
// handle invalid http method directly
if app.methodInt(c.Method()) == -1 {
_ = c.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
defer app.ReleaseCtx(ctx)
// Check if the HTTP method is valid
if ctx.methodINT == -1 {
_ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
return
}
// check flash messages
if strings.Contains(utils.UnsafeString(c.Request().Header.RawHeaders()), FlashCookieName) {
c.Redirect().parseAndClearFlashMessages()
// Optional: Check flash messages
rawHeaders := ctx.Request().Header.RawHeaders()
if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
ctx.Redirect().parseAndClearFlashMessages()
}
// Find match in stack
var err error
if app.newCtxFunc != nil {
_, err = app.nextCustom(c)
} else {
_, err = app.next(c.(*DefaultCtx)) //nolint:errcheck // It is fine to ignore the error here
}
// Attempt to match a route and execute the chain
_, err := app.next(ctx)
if err != nil {
if catch := c.App().ErrorHandler(c, err); catch != nil {
_ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here
if catch := ctx.App().ErrorHandler(ctx, err); catch != nil {
_ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil
}
// TODO: Do we need to return here?
}
}
func (app *App) customRequestHandler(rctx *fasthttp.RequestCtx) {
// Acquire CustomCtx from the pool
ctx, ok := app.AcquireCtx(rctx).(CustomCtx)
if !ok {
panic(errors.New("requestHandler: failed to type-assert to CustomCtx"))
}
defer app.ReleaseCtx(ctx)
// Check if the HTTP method is valid
if app.methodInt(ctx.Method()) == -1 {
_ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
return
}
// Optional: Check flash messages
rawHeaders := ctx.Request().Header.RawHeaders()
if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
ctx.Redirect().parseAndClearFlashMessages()
}
// Attempt to match a route and execute the chain
_, err := app.nextCustom(ctx)
if err != nil {
if catch := ctx.App().ErrorHandler(ctx, err); catch != nil {
_ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil
}
// TODO: Do we need to return here?
}
@ -290,81 +319,65 @@ func (*App) copyRoute(route *Route) *Route {
}
}
func (app *App) register(methods []string, pathRaw string, group *Group, handler Handler, middleware ...Handler) {
handlers := middleware
if handler != nil {
handlers = append(handlers, handler)
func (app *App) register(methods []string, pathRaw string, group *Group, handlers ...Handler) {
// A regular route requires at least one ctx handler
if len(handlers) == 0 && group == nil {
panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw))
}
// No nil handlers allowed
for _, h := range handlers {
if nil == h {
panic(fmt.Sprintf("nil handler in route: %s\n", pathRaw))
}
}
// Precompute path normalization ONCE
if pathRaw == "" {
pathRaw = "/"
}
if pathRaw[0] != '/' {
pathRaw = "/" + pathRaw
}
pathPretty := pathRaw
if !app.config.CaseSensitive {
pathPretty = utils.ToLower(pathPretty)
}
if !app.config.StrictRouting && len(pathPretty) > 1 {
pathPretty = utils.TrimRight(pathPretty, '/')
}
pathClean := RemoveEscapeChar(pathPretty)
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
isMount := group != nil && group.app != app
for _, method := range methods {
// Uppercase HTTP methods
method = utils.ToUpper(method)
// Check if the HTTP method is valid unless it's USE
if method != methodUse && app.methodInt(method) == -1 {
panic(fmt.Sprintf("add: invalid http method %s\n", method))
}
// Duplicate Route Handling
if app.routeExists(method, pathRaw) {
matchPathFunc := func(r *Route) bool { return r.Path == pathRaw }
app.deleteRoute([]string{method}, matchPathFunc)
}
// is mounted app
isMount := group != nil && group.app != app
// A route requires atleast one ctx handler
if len(handlers) == 0 && !isMount {
panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw))
}
// Cannot have an empty path
if pathRaw == "" {
pathRaw = "/"
}
// Path always start with a '/'
if pathRaw[0] != '/' {
pathRaw = "/" + pathRaw
}
// Create a stripped path in case-sensitive / trailing slashes
pathPretty := pathRaw
// Case-sensitive routing, all to lowercase
if !app.config.CaseSensitive {
pathPretty = utils.ToLower(pathPretty)
}
// Strict routing, remove trailing slashes
if !app.config.StrictRouting && len(pathPretty) > 1 {
pathPretty = utils.TrimRight(pathPretty, '/')
}
// Is layer a middleware?
isUse := method == methodUse
// Is path a direct wildcard?
isStar := pathPretty == "/*"
// Is path a root slash?
isRoot := pathPretty == "/"
// Parse path parameters
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
isStar := pathClean == "/*"
isRoot := pathClean == "/"
// Create route metadata without pointer
route := Route{
// Router booleans
use: isUse,
mount: isMount,
star: isStar,
root: isRoot,
// Path data
path: RemoveEscapeChar(pathPretty),
path: pathClean,
routeParser: parsedPretty,
Params: parsedRaw.params,
group: group,
// Group data
group: group,
// Public data
Path: pathRaw,
Method: method,
Handlers: handlers,
}
// Increment global handler count
atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) //nolint:gosec // Not a concern

View File

@ -7,7 +7,6 @@ package fiber
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
@ -35,6 +34,39 @@ func init() {
}
}
func Test_Route_Handler_Order(t *testing.T) {
t.Parallel()
app := New()
var order []int
handler1 := func(c Ctx) error {
order = append(order, 1)
return c.Next()
}
handler2 := func(c Ctx) error {
order = append(order, 2)
return c.Next()
}
handler3 := func(c Ctx) error {
order = append(order, 3)
return c.Next()
}
app.Get("/test", handler1, handler2, handler3, func(c Ctx) error {
order = append(order, 4)
return c.SendStatus(200)
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, 200, resp.StatusCode, "Status code")
expectedOrder := []int{1, 2, 3, 4}
require.Equal(t, expectedOrder, order, "Handler order")
}
func Test_Route_Match_SameLength(t *testing.T) {
t.Parallel()
@ -298,12 +330,22 @@ func Test_Router_Register_Missing_Handler(t *testing.T) {
t.Parallel()
app := New()
defer func() {
if err := recover(); err != nil {
require.Equal(t, "missing handler/middleware in route: /doe\n", fmt.Sprintf("%v", err))
}
}()
app.register([]string{"USE"}, "/doe", nil, nil)
t.Run("No Handler", func(t *testing.T) {
t.Parallel()
require.PanicsWithValue(t, "missing handler/middleware in route: /doe\n", func() {
app.register([]string{"USE"}, "/doe", nil)
})
})
t.Run("Nil Handler", func(t *testing.T) {
t.Parallel()
require.PanicsWithValue(t, "nil handler in route: /doe\n", func() {
app.register([]string{"USE"}, "/doe", nil, nil)
})
})
}
func Test_Ensure_Router_Interface_Implementation(t *testing.T) {
@ -834,6 +876,29 @@ func Benchmark_Router_Next_Default(b *testing.B) {
}
}
// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel$ github.com/gofiber/fiber/v3 -count=1
func Benchmark_Router_Next_Default_Parallel(b *testing.B) {
app := New()
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