mirror of https://github.com/gofiber/fiber.git
Merge branch 'main' into feature/3098-allow-removing-registered-route
commit
21e42ad126
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# ignore files or directories to be scanned by codecov
|
||||
ignore:
|
||||
- "./docs/"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5%
|
|
@ -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
|
||||
|
|
|
@ -12,6 +12,7 @@ changelog:
|
|||
- title: '🧹 Updates'
|
||||
labels:
|
||||
- '🧹 Updates'
|
||||
- '⚡️ Performance'
|
||||
- title: '🐛 Bug Fixes'
|
||||
labels:
|
||||
- '☢️ Bug'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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
|
||||
|
|
|
@ -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
97
app.go
|
@ -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
|
||||
|
|
345
app_test.go
345
app_test.go
|
@ -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) {
|
||||
|
|
103
bind_test.go
103
bind_test.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
36
ctx.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
182
ctx_test.go
182
ctx_test.go
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
```
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
1976
docs/api/ctx.md
1976
docs/api/ctx.md
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"label": "\uD83C\uDF0E Client",
|
||||
"position": 5,
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "HTTP client for Fiber."
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"label": "\uD83E\uDDE9 Extra",
|
||||
"position": 6,
|
||||
"position": 8,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Extra contents for Fiber."
|
||||
|
|
|
@ -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
|
|||
|
||||

|
||||
|
||||
## 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).
|
||||
|
|
|
@ -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 user‑supplied **CustomCtx**), and holding the router stack with all registered routes and groups. In addition, the App contains mount fields to support sub‑applications 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 (Sub‑Apps)"]
|
||||
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. case‑sensitivity, 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 route‑matching.
|
||||
- MountFields: Support for mounting sub‑applications 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
|
||||
|
||||
Fiber’s 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 request’s 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 pre‑built route tree and find a matching route based on the URL and HTTP method.
|
||||
6. Middleware Chain Execution: The matched route’s 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 route‑matching 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 sub‑routers. 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<int>
|
||||
Here, the segment \:userId<int> 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 route’s metadata for validation at runtime.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P["Route Pattern String<br/>(e.g., '/api/\\:userId\\<int>')"]
|
||||
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 request’s Context with the actual values parsed from the URL.
|
||||
|
||||
## Route Matching and Parameter Extraction
|
||||
|
||||
When a request is processed, Fiber uses its pre‑computed 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 pre‑grouped 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 sub‑applications (or sub‑routers) 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 sub‑app’s mount fields are updated with the prefix of the parent, and its routes are integrated into the parent’s routing structure.
|
||||
3. Processing Sub‑App Routes: During startup, the parent app collects routes from mounted sub‑apps 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 Fiber’s 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 route’s 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 request‑specific 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 memory‑efficient even under heavy load.
|
||||
|
||||
## Preforking Mechanism
|
||||
|
||||
To take full advantage of multi‑core 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 Unix‑like systems), the child terminates gracefully.
|
||||
|
||||
### Detailed Preforking Workflow
|
||||
|
||||
Fiber’s prefork mode uses OS‑level mechanisms to allow multiple processes to listen on the same port. Here’s 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
|
||||
|
||||
Fiber’s 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 post‑redirect‑get 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 sub‑application 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
|
||||
|
||||
Fiber’s DefaultCtx (or CustomCtx) represents the per‑request 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.
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"label": "\uD83D\uDCD6 Guide",
|
||||
"position": 5,
|
||||
"position": 7,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Guides for Fiber."
|
||||
|
|
|
@ -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.
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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` |
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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(¶ms); err != nil {
|
||||
if err := c.Bind().URI(¶ms); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(params)
|
||||
|
|
2
error.go
2
error.go
|
@ -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
17
go.mod
|
@ -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
32
go.sum
|
@ -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=
|
||||
|
|
48
group.go
48
group.go
|
@ -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
|
||||
|
|
65
helpers.go
65
helpers.go
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
80
hooks.go
80
hooks.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
28
listen.go
28
listen.go
|
@ -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)
|
||||
}
|
||||
|
|
223
listen_test.go
223
listen_test.go
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
4
mount.go
4
mount.go
|
@ -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
141
path.go
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
66
register.go
66
register.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
233
router.go
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue