mirror of https://github.com/gofiber/fiber.git
Merge remote-tracking branch 'origin/main'
commit
0bf0353a13
|
@ -124,7 +124,7 @@ We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues),
|
||||||
|
|
||||||
## ⚠️ Limitations
|
## ⚠️ 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.
|
- 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
|
## 👀 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 |
|
| [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. |
|
| [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
|
## 🕶️ Awesome List
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# ignore files or directories to be scanned by codecov
|
||||||
|
ignore:
|
||||||
|
- "./docs/"
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
threshold: 0.5%
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
unit:
|
unit:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.23.x]
|
go-version: [1.23.x, 1.24.x]
|
||||||
platform: [ubuntu-latest, windows-latest, macos-latest, macos-13]
|
platform: [ubuntu-latest, windows-latest, macos-latest, macos-13]
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|
71
app.go
71
app.go
|
@ -750,7 +750,7 @@ func (app *App) Use(args ...any) Router {
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
app.register([]string{methodUse}, prefix, nil, nil, handlers...)
|
app.register([]string{methodUse}, prefix, nil, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -758,67 +758,67 @@ func (app *App) Use(args ...any) Router {
|
||||||
|
|
||||||
// Get registers a route for GET methods that requests a representation
|
// Get registers a route for GET methods that requests a representation
|
||||||
// of the specified resource. Requests using GET should only retrieve data.
|
// of the specified resource. Requests using GET should only retrieve data.
|
||||||
func (app *App) Get(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Get(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodGet}, path, handler, middleware...)
|
return app.Add([]string{MethodGet}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Head registers a route for HEAD methods that asks for a response identical
|
// Head registers a route for HEAD methods that asks for a response identical
|
||||||
// to that of a GET request, but without the response body.
|
// to that of a GET request, but without the response body.
|
||||||
func (app *App) Head(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Head(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodHead}, path, handler, middleware...)
|
return app.Add([]string{MethodHead}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post registers a route for POST methods that is used to submit an entity to the
|
// 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.
|
// 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 {
|
func (app *App) Post(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodPost}, path, handler, middleware...)
|
return app.Add([]string{MethodPost}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put registers a route for PUT methods that replaces all current representations
|
// Put registers a route for PUT methods that replaces all current representations
|
||||||
// of the target resource with the request payload.
|
// of the target resource with the request payload.
|
||||||
func (app *App) Put(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Put(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodPut}, path, handler, middleware...)
|
return app.Add([]string{MethodPut}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete registers a route for DELETE methods that deletes the specified resource.
|
// Delete registers a route for DELETE methods that deletes the specified resource.
|
||||||
func (app *App) Delete(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Delete(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodDelete}, path, handler, middleware...)
|
return app.Add([]string{MethodDelete}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect registers a route for CONNECT methods that establishes a tunnel to the
|
// Connect registers a route for CONNECT methods that establishes a tunnel to the
|
||||||
// server identified by the target resource.
|
// server identified by the target resource.
|
||||||
func (app *App) Connect(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodConnect}, path, handler, middleware...)
|
return app.Add([]string{MethodConnect}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options registers a route for OPTIONS methods that is used to describe the
|
// Options registers a route for OPTIONS methods that is used to describe the
|
||||||
// communication options for the target resource.
|
// communication options for the target resource.
|
||||||
func (app *App) Options(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Options(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodOptions}, path, handler, middleware...)
|
return app.Add([]string{MethodOptions}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace registers a route for TRACE methods that performs a message loop-back
|
// Trace registers a route for TRACE methods that performs a message loop-back
|
||||||
// test along the path to the target resource.
|
// test along the path to the target resource.
|
||||||
func (app *App) Trace(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodTrace}, path, handler, middleware...)
|
return app.Add([]string{MethodTrace}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch registers a route for PATCH methods that is used to apply partial
|
// Patch registers a route for PATCH methods that is used to apply partial
|
||||||
// modifications to a resource.
|
// modifications to a resource.
|
||||||
func (app *App) Patch(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) Patch(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add([]string{MethodPatch}, path, handler, middleware...)
|
return app.Add([]string{MethodPatch}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add allows you to specify multiple HTTP methods to register a route.
|
// 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 {
|
func (app *App) Add(methods []string, path string, handler Handler, handlers ...Handler) Router {
|
||||||
app.register(methods, path, nil, handler, middleware...)
|
app.register(methods, path, nil, append([]Handler{handler}, handlers...)...)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// All will register the handler on all HTTP methods
|
// All will register the handler on all HTTP methods
|
||||||
func (app *App) All(path string, handler Handler, middleware ...Handler) Router {
|
func (app *App) All(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return app.Add(app.config.RequestMethods, path, handler, middleware...)
|
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.
|
// Group is used for Routes with common prefix to define a new sub-router with optional middleware.
|
||||||
|
@ -828,7 +828,7 @@ func (app *App) All(path string, handler Handler, middleware ...Handler) Router
|
||||||
func (app *App) Group(prefix string, handlers ...Handler) Router {
|
func (app *App) Group(prefix string, handlers ...Handler) Router {
|
||||||
grp := &Group{Prefix: prefix, app: app}
|
grp := &Group{Prefix: prefix, app: app}
|
||||||
if len(handlers) > 0 {
|
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 {
|
if err := app.hooks.executeOnGroupHooks(*grp); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -894,6 +894,13 @@ func (app *App) HandlersCount() uint32 {
|
||||||
//
|
//
|
||||||
// Make sure the program doesn't exit and waits instead for Shutdown to return.
|
// 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.
|
// Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.
|
||||||
func (app *App) Shutdown() error {
|
func (app *App) Shutdown() error {
|
||||||
return app.ShutdownWithContext(context.Background())
|
return app.ShutdownWithContext(context.Background())
|
||||||
|
@ -918,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.
|
// 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 {
|
func (app *App) ShutdownWithContext(ctx context.Context) error {
|
||||||
if app.hooks != nil {
|
|
||||||
// TODO: check should be defered?
|
|
||||||
app.hooks.executeOnShutdownHooks()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.mutex.Lock()
|
app.mutex.Lock()
|
||||||
defer app.mutex.Unlock()
|
defer app.mutex.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
if app.server == nil {
|
if app.server == nil {
|
||||||
return ErrNotRunning
|
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
|
// Server returns the underlying fasthttp server
|
||||||
|
|
192
app_test.go
192
app_test.go
|
@ -21,6 +21,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -479,14 +480,10 @@ func Test_App_Use_Params(t *testing.T) {
|
||||||
require.NoError(t, err, "app.Test(req)")
|
require.NoError(t, err, "app.Test(req)")
|
||||||
require.Equal(t, 200, resp.StatusCode, "Status code")
|
require.Equal(t, 200, resp.StatusCode, "Status code")
|
||||||
|
|
||||||
defer func() {
|
require.PanicsWithValue(t, "use: invalid handler func()\n", func() {
|
||||||
if err := recover(); err != nil {
|
app.Use("/:param/*", func() {
|
||||||
require.Equal(t, "use: invalid handler func()\n", fmt.Sprintf("%v", err))
|
// this should panic
|
||||||
}
|
})
|
||||||
}()
|
|
||||||
|
|
||||||
app.Use("/:param/*", func() {
|
|
||||||
// this should panic
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -930,20 +927,29 @@ func Test_App_ShutdownWithTimeout(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ln := fasthttputil.NewInmemoryListener()
|
ln := fasthttputil.NewInmemoryListener()
|
||||||
|
serverReady := make(chan struct{}) // Signal that the server is ready to start
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
serverReady <- struct{}{}
|
||||||
err := app.Listener(ln)
|
err := app.Listener(ln)
|
||||||
assert.NoError(t, err)
|
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() {
|
go func() {
|
||||||
conn, err := ln.Dial()
|
conn, err := ln.Dial()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n"))
|
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n"))
|
||||||
assert.NoError(t, err)
|
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)
|
shutdownErr := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -964,46 +970,130 @@ func Test_App_ShutdownWithTimeout(t *testing.T) {
|
||||||
func Test_App_ShutdownWithContext(t *testing.T) {
|
func Test_App_ShutdownWithContext(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
app := New()
|
t.Run("successful shutdown", func(t *testing.T) {
|
||||||
app.Get("/", func(ctx Ctx) error {
|
t.Parallel()
|
||||||
time.Sleep(5 * time.Second)
|
app := New()
|
||||||
return ctx.SendString("body")
|
|
||||||
|
// 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() {
|
hookOrder := make([]string, 0)
|
||||||
err := app.Listener(ln)
|
var hookMutex sync.Mutex
|
||||||
assert.NoError(t, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
app.Hooks().OnPreShutdown(func() error {
|
||||||
|
hookMutex.Lock()
|
||||||
|
hookOrder = append(hookOrder, "pre")
|
||||||
|
hookMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
go func() {
|
app.Hooks().OnPostShutdown(func(_ error) error {
|
||||||
conn, err := ln.Dial()
|
hookMutex.Lock()
|
||||||
assert.NoError(t, err)
|
hookOrder = append(hookOrder, "post")
|
||||||
|
hookMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n"))
|
ln := fasthttputil.NewInmemoryListener()
|
||||||
assert.NoError(t, err)
|
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)
|
err := app.ShutdownWithContext(context.Background())
|
||||||
go func() {
|
require.NoError(t, err)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
shutdownErr <- app.ShutdownWithContext(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
require.Equal(t, []string{"pre", "post"}, hookOrder, "Hooks should execute in order")
|
||||||
case <-time.After(5 * time.Second):
|
})
|
||||||
t.Fatal("idle connections not closed on shutdown")
|
|
||||||
case err := <-shutdownErr:
|
t.Run("timeout with long running request", func(t *testing.T) {
|
||||||
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
|
t.Parallel()
|
||||||
t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded)
|
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
|
// go test -run Test_App_Mixed_Routes_WithSameLen
|
||||||
|
@ -1055,12 +1145,10 @@ func Test_App_Mixed_Routes_WithSameLen(t *testing.T) {
|
||||||
|
|
||||||
func Test_App_Group_Invalid(t *testing.T) {
|
func Test_App_Group_Invalid(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
require.PanicsWithValue(t, "use: invalid handler int\n", func() {
|
||||||
require.Equal(t, "use: invalid handler int\n", fmt.Sprintf("%v", err))
|
New().Group("/").Use(1)
|
||||||
}
|
})
|
||||||
}()
|
|
||||||
New().Group("/").Use(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_App_Group(t *testing.T) {
|
func Test_App_Group(t *testing.T) {
|
||||||
|
@ -1285,14 +1373,10 @@ func Test_App_Init_Error_View(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
app := New(Config{Views: invalidView{}})
|
app := New(Config{Views: invalidView{}})
|
||||||
|
|
||||||
defer func() {
|
require.PanicsWithValue(t, "implement me", func() {
|
||||||
if err := recover(); err != nil {
|
//nolint:errcheck // not needed
|
||||||
require.Equal(t, "implement me", fmt.Sprintf("%v", err))
|
_ = app.config.Views.Render(nil, "", nil)
|
||||||
}
|
})
|
||||||
}()
|
|
||||||
|
|
||||||
err := app.config.Views.Render(nil, "", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// go test -run Test_App_Stack
|
// go test -run Test_App_Stack
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package binder
|
package binder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
"github.com/gofiber/utils/v2"
|
"github.com/gofiber/utils/v2"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
@ -59,7 +61,15 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parse(b.Name(), out, data)
|
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.
|
// Reset resets the FormBinding binder.
|
||||||
|
|
|
@ -2,6 +2,7 @@ package binder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -98,10 +99,12 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Name string `form:"name"`
|
Avatar *multipart.FileHeader `form:"avatar"`
|
||||||
Names []string `form:"names"`
|
Name string `form:"name"`
|
||||||
Posts []Post `form:"posts"`
|
Names []string `form:"names"`
|
||||||
Age int `form:"age"`
|
Posts []Post `form:"posts"`
|
||||||
|
Avatars []*multipart.FileHeader `form:"avatars"`
|
||||||
|
Age int `form:"age"`
|
||||||
}
|
}
|
||||||
var user User
|
var user User
|
||||||
|
|
||||||
|
@ -118,6 +121,24 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
|
||||||
require.NoError(t, mw.WriteField("posts[1][title]", "post2"))
|
require.NoError(t, mw.WriteField("posts[1][title]", "post2"))
|
||||||
require.NoError(t, mw.WriteField("posts[2][title]", "post3"))
|
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())
|
require.NoError(t, mw.Close())
|
||||||
|
|
||||||
req.Header.SetContentType(mw.FormDataContentType())
|
req.Header.SetContentType(mw.FormDataContentType())
|
||||||
|
@ -127,7 +148,7 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
|
||||||
fasthttp.ReleaseRequest(req)
|
fasthttp.ReleaseRequest(req)
|
||||||
})
|
})
|
||||||
|
|
||||||
err := b.Bind(req, &user)
|
err = b.Bind(req, &user)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, "john", user.Name)
|
require.Equal(t, "john", user.Name)
|
||||||
|
@ -139,6 +160,38 @@ func Test_FormBinder_BindMultipart(t *testing.T) {
|
||||||
require.Equal(t, "post1", user.Posts[0].Title)
|
require.Equal(t, "post1", user.Posts[0].Title)
|
||||||
require.Equal(t, "post2", user.Posts[1].Title)
|
require.Equal(t, "post2", user.Posts[1].Title)
|
||||||
require.Equal(t, "post3", user.Posts[2].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) {
|
func Benchmark_FormBinder_BindMultipart(b *testing.B) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package binder
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -69,7 +70,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse data into the map or struct
|
// 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)
|
ptrVal := reflect.ValueOf(out)
|
||||||
|
|
||||||
// Get pointer value
|
// Get pointer value
|
||||||
|
@ -83,11 +84,11 @@ func parse(aliasTag string, out any, data map[string][]string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse into the struct
|
// Parse into the struct
|
||||||
return parseToStruct(aliasTag, out, data)
|
return parseToStruct(aliasTag, out, data, files...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse data into the struct with gorilla/schema
|
// Parse data into the struct with gorilla/schema
|
||||||
func parseToStruct(aliasTag string, out any, data map[string][]string) error {
|
func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error {
|
||||||
// Get decoder from pool
|
// Get decoder from pool
|
||||||
schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed
|
schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed
|
||||||
defer decoderPoolMap[aliasTag].Put(schemaDecoder)
|
defer decoderPoolMap[aliasTag].Put(schemaDecoder)
|
||||||
|
@ -95,7 +96,7 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error {
|
||||||
// Set alias tag
|
// Set alias tag
|
||||||
schemaDecoder.SetAliasTag(aliasTag)
|
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)
|
return fmt.Errorf("bind: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,7 +251,7 @@ func FilterFlags(content string) string {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatBindData[T any](out any, data map[string][]string, key string, value T, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay
|
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
|
var err error
|
||||||
if supportBracketNotation && strings.Contains(key, "[") {
|
if supportBracketNotation && strings.Contains(key, "[") {
|
||||||
key, err = parseParamSquareBrackets(key)
|
key, err = parseParamSquareBrackets(key)
|
||||||
|
@ -261,10 +262,28 @@ func formatBindData[T any](out any, data map[string][]string, key string, value
|
||||||
|
|
||||||
switch v := any(value).(type) {
|
switch v := any(value).(type) {
|
||||||
case string:
|
case string:
|
||||||
assignBindData(out, data, key, v, enableSplitting)
|
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:
|
case []string:
|
||||||
|
dataMap, ok := any(data).(map[string][]string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unsupported value type: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
for _, val := range v {
|
for _, val := range v {
|
||||||
assignBindData(out, data, key, val, enableSplitting)
|
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:
|
default:
|
||||||
return fmt.Errorf("unsupported value type: %T", value)
|
return fmt.Errorf("unsupported value type: %T", value)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package binder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -9,6 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_EqualFieldType(t *testing.T) {
|
func Test_EqualFieldType(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
var out int
|
var out int
|
||||||
require.False(t, equalFieldType(&out, reflect.Int, "key"))
|
require.False(t, equalFieldType(&out, reflect.Int, "key"))
|
||||||
|
|
||||||
|
@ -47,6 +50,8 @@ func Test_EqualFieldType(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_ParseParamSquareBrackets(t *testing.T) {
|
func Test_ParseParamSquareBrackets(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
err error
|
err error
|
||||||
input string
|
input string
|
||||||
|
@ -101,6 +106,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
result, err := parseParamSquareBrackets(tt.input)
|
result, err := parseParamSquareBrackets(tt.input)
|
||||||
if tt.err != nil {
|
if tt.err != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
@ -114,6 +121,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_parseToMap(t *testing.T) {
|
func Test_parseToMap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
inputMap := map[string][]string{
|
inputMap := map[string][]string{
|
||||||
"key1": {"value1", "value2"},
|
"key1": {"value1", "value2"},
|
||||||
"key2": {"value3"},
|
"key2": {"value3"},
|
||||||
|
@ -147,6 +156,8 @@ func Test_parseToMap(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_FilterFlags(t *testing.T) {
|
func Test_FilterFlags(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
expected string
|
expected string
|
||||||
|
@ -172,8 +183,163 @@ func Test_FilterFlags(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
result := FilterFlags(tt.input)
|
result := FilterFlags(tt.input)
|
||||||
require.Equal(t, tt.expected, result)
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -3,24 +3,22 @@ package client
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand/v2"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/utils/v2"
|
"github.com/gofiber/utils/v2"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var protocolCheck = regexp.MustCompile(`^https?://.*$`)
|
||||||
protocolCheck = regexp.MustCompile(`^https?://.*$`)
|
|
||||||
|
|
||||||
headerAccept = "Accept"
|
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerAccept = "Accept"
|
||||||
applicationJSON = "application/json"
|
applicationJSON = "application/json"
|
||||||
applicationCBOR = "application/cbor"
|
applicationCBOR = "application/cbor"
|
||||||
applicationXML = "application/xml"
|
applicationXML = "application/xml"
|
||||||
|
@ -30,25 +28,26 @@ var (
|
||||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
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.
|
// unsafeRandString returns a random string of length n.
|
||||||
func randString(n int) string {
|
func unsafeRandString(n int) string {
|
||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
length := len(letterBytes)
|
const length = uint64(len(letterBytes))
|
||||||
src := rand.NewSource(time.Now().UnixNano())
|
|
||||||
|
|
||||||
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 {
|
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]
|
b[i] = letterBytes[idx]
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
cache >>= int64(letterIdxBits)
|
cache >>= letterIdxBits
|
||||||
remain--
|
remain--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +133,7 @@ func parserRequestHeader(c *Client, req *Request) error {
|
||||||
req.RawRequest.Header.SetContentType(multipartFormData)
|
req.RawRequest.Header.SetContentType(multipartFormData)
|
||||||
// If boundary is default, append a random string to it.
|
// If boundary is default, append a random string to it.
|
||||||
if req.boundary == boundary {
|
if req.boundary == boundary {
|
||||||
req.boundary += randString(16)
|
req.boundary += unsafeRandString(16)
|
||||||
}
|
}
|
||||||
req.RawRequest.Header.SetMultipartFormBoundary(req.boundary)
|
req.RawRequest.Header.SetMultipartFormBoundary(req.boundary)
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -38,7 +38,7 @@ func Test_Rand_String(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
got := randString(tt.args)
|
got := unsafeRandString(tt.args)
|
||||||
require.Len(t, got, tt.args)
|
require.Len(t, got, tt.args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
1
ctx.go
1
ctx.go
|
@ -1555,6 +1555,7 @@ func (c *DefaultCtx) SendFile(file string, config ...SendFile) error {
|
||||||
AcceptByteRange: cfg.ByteRange,
|
AcceptByteRange: cfg.ByteRange,
|
||||||
Compress: cfg.Compress,
|
Compress: cfg.Compress,
|
||||||
CompressBrotli: cfg.Compress,
|
CompressBrotli: cfg.Compress,
|
||||||
|
CompressZstd: cfg.Compress,
|
||||||
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
|
CompressedFileSuffixes: c.app.config.CompressedFileSuffixes,
|
||||||
CacheDuration: cfg.CacheDuration,
|
CacheDuration: cfg.CacheDuration,
|
||||||
SkipCache: cfg.CacheDuration < 0,
|
SkipCache: cfg.CacheDuration < 0,
|
||||||
|
|
|
@ -135,18 +135,18 @@ func (app *App) Route(path string) Register
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Register interface {
|
type Register interface {
|
||||||
All(handler Handler, middleware ...Handler) Register
|
All(handler Handler, handlers ...Handler) Register
|
||||||
Get(handler Handler, middleware ...Handler) Register
|
Get(handler Handler, handlers ...Handler) Register
|
||||||
Head(handler Handler, middleware ...Handler) Register
|
Head(handler Handler, handlers ...Handler) Register
|
||||||
Post(handler Handler, middleware ...Handler) Register
|
Post(handler Handler, handlers ...Handler) Register
|
||||||
Put(handler Handler, middleware ...Handler) Register
|
Put(handler Handler, handlers ...Handler) Register
|
||||||
Delete(handler Handler, middleware ...Handler) Register
|
Delete(handler Handler, handlers ...Handler) Register
|
||||||
Connect(handler Handler, middleware ...Handler) Register
|
Connect(handler Handler, handlers ...Handler) Register
|
||||||
Options(handler Handler, middleware ...Handler) Register
|
Options(handler Handler, handlers ...Handler) Register
|
||||||
Trace(handler Handler, middleware ...Handler) Register
|
Trace(handler Handler, handlers ...Handler) Register
|
||||||
Patch(handler Handler, middleware ...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
|
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
|
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
|
### JSON
|
||||||
|
|
||||||
Binds the request JSON body to a struct.
|
Binds the request JSON body to a struct.
|
||||||
|
|
|
@ -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="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="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="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="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="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="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="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` |
|
| <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.
|
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
|
```go
|
||||||
func (app *App) Shutdown() error
|
func (app *App) Shutdown() error
|
||||||
|
|
|
@ -15,7 +15,8 @@ With Fiber you can execute custom user functions at specific method execution po
|
||||||
- [OnGroupName](#ongroupname)
|
- [OnGroupName](#ongroupname)
|
||||||
- [OnListen](#onlisten)
|
- [OnListen](#onlisten)
|
||||||
- [OnFork](#onfork)
|
- [OnFork](#onfork)
|
||||||
- [OnShutdown](#onshutdown)
|
- [OnPreShutdown](#onpreshutdown)
|
||||||
|
- [OnPostShutdown](#onpostshutdown)
|
||||||
- [OnMount](#onmount)
|
- [OnMount](#onmount)
|
||||||
|
|
||||||
## Constants
|
## Constants
|
||||||
|
@ -28,7 +29,8 @@ type OnGroupHandler = func(Group) error
|
||||||
type OnGroupNameHandler = OnGroupHandler
|
type OnGroupNameHandler = OnGroupHandler
|
||||||
type OnListenHandler = func(ListenData) error
|
type OnListenHandler = func(ListenData) error
|
||||||
type OnForkHandler = func(int) error
|
type OnForkHandler = func(int) error
|
||||||
type OnShutdownHandler = func() error
|
type OnPreShutdownHandler = func() error
|
||||||
|
type OnPostShutdownHandler = func(error) error
|
||||||
type OnMountHandler = func(*App) error
|
type OnMountHandler = func(*App) error
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -174,12 +176,20 @@ func main() {
|
||||||
func (h *Hooks) OnFork(handler ...OnForkHandler)
|
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"
|
```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
|
## OnMount
|
||||||
|
|
|
@ -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.
|
[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.
|
||||||
|
|
||||||
|
@ -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:
|
Yes we do, here are some examples:
|
||||||
This example works v2
|
|
||||||
|
<details>
|
||||||
|
<summary>Example</summary>
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
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)
|
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).
|
||||||
|
|
|
@ -4,24 +4,34 @@ id: adaptor
|
||||||
|
|
||||||
# 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
|
- Convert `net/http` handlers and middleware to Fiber handlers.
|
||||||
| :--- | :--- | :---
|
- Convert Fiber handlers to `net/http` handlers.
|
||||||
| HTTPHandler | `HTTPHandler(h http.Handler) fiber.Handler` | http.Handler -> fiber.Handler
|
- Convert Fiber context (`fiber.Ctx`) into an `http.Request`.
|
||||||
| 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
|
|
||||||
|
|
||||||
## 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
|
```go
|
||||||
package main
|
package main
|
||||||
|
@ -29,35 +39,27 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// New fiber app
|
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
// http.Handler -> fiber.Handler
|
// Convert a http.Handler to a Fiber handler
|
||||||
app.Get("/", adaptor.HTTPHandler(handler(greet)))
|
app.Get("/", adaptor.HTTPHandler(http.HandlerFunc(helloHandler)))
|
||||||
|
|
||||||
// http.HandlerFunc -> fiber.Handler
|
|
||||||
app.Get("/func", adaptor.HTTPHandlerFunc(greet))
|
|
||||||
|
|
||||||
// Listen on port 3000
|
|
||||||
app.Listen(":3000")
|
app.Listen(":3000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handler(f http.HandlerFunc) http.Handler {
|
func helloHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return http.HandlerFunc(f)
|
fmt.Fprint(w, "Hello from net/http!")
|
||||||
}
|
|
||||||
|
|
||||||
func greet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprint(w, "Hello World!")
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### net/http middleware to Fiber
|
### 2. Using `net/http` Middleware with Fiber
|
||||||
|
|
||||||
|
Middleware written for `net/http` can be used in Fiber:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
@ -65,111 +67,119 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// New fiber app
|
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
// http middleware -> fiber.Handler
|
// Apply a http middleware in Fiber
|
||||||
app.Use(adaptor.HTTPMiddleware(logMiddleware))
|
app.Use(adaptor.HTTPMiddleware(loggingMiddleware))
|
||||||
|
|
||||||
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
|
return c.SendString("Hello Fiber!")
|
||||||
|
})
|
||||||
|
|
||||||
// Listen on port 3000
|
|
||||||
app.Listen(":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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Println("log middleware")
|
log.Println("Request received")
|
||||||
next.ServeHTTP(w, r)
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// fiber.Handler -> http.Handler
|
// Convert Fiber handler to an http.Handler
|
||||||
http.Handle("/", adaptor.FiberHandler(greet))
|
http.Handle("/", adaptor.FiberHandler(helloFiber))
|
||||||
|
|
||||||
// fiber.Handler -> http.HandlerFunc
|
// Convert Fiber handler to http.HandlerFunc
|
||||||
http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet))
|
http.HandleFunc("/func", adaptor.FiberHandlerFunc(helloFiber))
|
||||||
|
|
||||||
// Listen on port 3000
|
|
||||||
http.ListenAndServe(":3000", nil)
|
http.ListenAndServe(":3000", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func greet(c fiber.Ctx) error {
|
func helloFiber(c fiber.Ctx) error {
|
||||||
return c.SendString("Hello World!")
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
app.Get("/", func(c fiber.Ctx) error {
|
||||||
|
return c.SendString("Hello from Fiber!")
|
||||||
|
})
|
||||||
|
|
||||||
app.Get("/greet", greet)
|
// Run Fiber inside an http server
|
||||||
|
|
||||||
// Listen on port 3000
|
|
||||||
http.ListenAndServe(":3000", adaptor.FiberApp(app))
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
"github.com/gofiber/fiber/v3/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
app.Get("/request", handleRequest)
|
||||||
app.Get("/greet", greetWithHTTPReq)
|
app.Listen(":3000")
|
||||||
|
|
||||||
// Listen on port 3000
|
|
||||||
http.ListenAndServe(":3000", adaptor.FiberApp(app))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func greetWithHTTPReq(c fiber.Ctx) error {
|
func handleRequest(c fiber.Ctx) error {
|
||||||
httpReq, err := adaptor.ConvertRequest(c, false)
|
httpReq, err := adaptor.ConvertRequest(c, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return c.SendString("Converted Request URL: " + httpReq.URL.String())
|
||||||
return c.SendString("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.
|
||||||
|
|
|
@ -9,22 +9,22 @@ Registers a route bound to a specific [HTTP method](https://developer.mozilla.or
|
||||||
|
|
||||||
```go title="Signatures"
|
```go title="Signatures"
|
||||||
// HTTP methods
|
// HTTP methods
|
||||||
func (app *App) Get(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, middlewares ...Handler) Router
|
func (app *App) Head(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Post(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Post(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Put(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Put(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Delete(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Delete(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Connect(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Connect(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Options(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Options(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Trace(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Trace(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Patch(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Patch(path string, handler Handler, handlers ...Handler) Router
|
||||||
|
|
||||||
// Add allows you to specify a method as value
|
// 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
|
// All will register the route on all HTTP methods
|
||||||
// Almost the same as app.Use but not bound to prefixes
|
// 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"
|
```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
|
func (app *App) Use(args ...any) Router
|
||||||
|
|
||||||
// Different usage variations
|
// Different usage variations
|
||||||
func (app *App) Use(handler Handler, middlewares ...Handler) Router
|
func (app *App) Use(handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Use(path string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Use(path string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Use(paths []string, handler Handler, middlewares ...Handler) Router
|
func (app *App) Use(paths []string, handler Handler, handlers ...Handler) Router
|
||||||
func (app *App) Use(path string, app *App) 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`:
|
Here's a quick overview of the changes in Fiber `v3`:
|
||||||
|
|
||||||
- [🚀 App](#-app)
|
- [🚀 App](#-app)
|
||||||
|
- [🎣 Hooks](#-hooks)
|
||||||
|
- [🚀 Listen](#-listen)
|
||||||
- [🗺️ Router](#-router)
|
- [🗺️ Router](#-router)
|
||||||
- [🧠 Context](#-context)
|
- [🧠 Context](#-context)
|
||||||
- [📎 Binding](#-binding)
|
- [📎 Binding](#-binding)
|
||||||
|
@ -158,6 +160,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
|
## 🗺 Router
|
||||||
|
|
||||||
We have slightly adapted our router interface
|
We have slightly adapted our router interface
|
||||||
|
@ -487,6 +546,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.
|
- Unified binding from URL parameters, query parameters, headers, and request bodies.
|
||||||
- Support for custom binders and constraints.
|
- Support for custom binders and constraints.
|
||||||
- Improved error handling and validation.
|
- Improved error handling and validation.
|
||||||
|
- Support multipart file binding for `*multipart.FileHeader`, `*[]*multipart.FileHeader`, and `[]*multipart.FileHeader` field types.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Example</summary>
|
<summary>Example</summary>
|
||||||
|
|
11
go.mod
11
go.mod
|
@ -1,9 +1,9 @@
|
||||||
module github.com/gofiber/fiber/v3
|
module github.com/gofiber/fiber/v3
|
||||||
|
|
||||||
go 1.23
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
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/gofiber/utils/v2 v2.0.0-beta.7
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mattn/go-colorable v0.1.14
|
github.com/mattn/go-colorable v0.1.14
|
||||||
|
@ -11,8 +11,8 @@ require (
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/tinylib/msgp v1.2.5
|
github.com/tinylib/msgp v1.2.5
|
||||||
github.com/valyala/bytebufferpool v1.0.0
|
github.com/valyala/bytebufferpool v1.0.0
|
||||||
github.com/valyala/fasthttp v1.58.0
|
github.com/valyala/fasthttp v1.59.0
|
||||||
golang.org/x/crypto v0.33.0
|
golang.org/x/crypto v0.35.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -22,9 +22,8 @@ require (
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
18
go.sum
18
go.sum
|
@ -4,8 +4,8 @@ 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/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 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
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.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q=
|
||||||
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
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 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
|
||||||
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
@ -26,18 +26,16 @@ github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
|
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
||||||
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
|
48
group.go
48
group.go
|
@ -97,7 +97,7 @@ func (grp *Group) Use(args ...any) Router {
|
||||||
return grp
|
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 {
|
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
|
// Get registers a route for GET methods that requests a representation
|
||||||
// of the specified resource. Requests using GET should only retrieve data.
|
// of the specified resource. Requests using GET should only retrieve data.
|
||||||
func (grp *Group) Get(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Get(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodGet}, path, handler, middleware...)
|
return grp.Add([]string{MethodGet}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Head registers a route for HEAD methods that asks for a response identical
|
// Head registers a route for HEAD methods that asks for a response identical
|
||||||
// to that of a GET request, but without the response body.
|
// to that of a GET request, but without the response body.
|
||||||
func (grp *Group) Head(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Head(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodHead}, path, handler, middleware...)
|
return grp.Add([]string{MethodHead}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post registers a route for POST methods that is used to submit an entity to the
|
// 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.
|
// 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 {
|
func (grp *Group) Post(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodPost}, path, handler, middleware...)
|
return grp.Add([]string{MethodPost}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put registers a route for PUT methods that replaces all current representations
|
// Put registers a route for PUT methods that replaces all current representations
|
||||||
// of the target resource with the request payload.
|
// of the target resource with the request payload.
|
||||||
func (grp *Group) Put(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Put(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodPut}, path, handler, middleware...)
|
return grp.Add([]string{MethodPut}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete registers a route for DELETE methods that deletes the specified resource.
|
// Delete registers a route for DELETE methods that deletes the specified resource.
|
||||||
func (grp *Group) Delete(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Delete(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodDelete}, path, handler, middleware...)
|
return grp.Add([]string{MethodDelete}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect registers a route for CONNECT methods that establishes a tunnel to the
|
// Connect registers a route for CONNECT methods that establishes a tunnel to the
|
||||||
// server identified by the target resource.
|
// server identified by the target resource.
|
||||||
func (grp *Group) Connect(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Connect(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodConnect}, path, handler, middleware...)
|
return grp.Add([]string{MethodConnect}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options registers a route for OPTIONS methods that is used to describe the
|
// Options registers a route for OPTIONS methods that is used to describe the
|
||||||
// communication options for the target resource.
|
// communication options for the target resource.
|
||||||
func (grp *Group) Options(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Options(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodOptions}, path, handler, middleware...)
|
return grp.Add([]string{MethodOptions}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace registers a route for TRACE methods that performs a message loop-back
|
// Trace registers a route for TRACE methods that performs a message loop-back
|
||||||
// test along the path to the target resource.
|
// test along the path to the target resource.
|
||||||
func (grp *Group) Trace(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Trace(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodTrace}, path, handler, middleware...)
|
return grp.Add([]string{MethodTrace}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch registers a route for PATCH methods that is used to apply partial
|
// Patch registers a route for PATCH methods that is used to apply partial
|
||||||
// modifications to a resource.
|
// modifications to a resource.
|
||||||
func (grp *Group) Patch(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) Patch(path string, handler Handler, handlers ...Handler) Router {
|
||||||
return grp.Add([]string{MethodPatch}, path, handler, middleware...)
|
return grp.Add([]string{MethodPatch}, path, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add allows you to specify multiple HTTP methods to register a route.
|
// 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 {
|
func (grp *Group) Add(methods []string, path string, handler Handler, handlers ...Handler) Router {
|
||||||
grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, handler, middleware...)
|
grp.app.register(methods, getGroupPath(grp.Prefix, path), grp, append([]Handler{handler}, handlers...)...)
|
||||||
if !grp.anyRouteDefined {
|
if !grp.anyRouteDefined {
|
||||||
grp.anyRouteDefined = true
|
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
|
// All will register the handler on all HTTP methods
|
||||||
func (grp *Group) All(path string, handler Handler, middleware ...Handler) Router {
|
func (grp *Group) All(path string, handler Handler, handlers ...Handler) Router {
|
||||||
_ = grp.Add(grp.app.config.RequestMethods, path, handler, middleware...)
|
_ = grp.Add(grp.app.config.RequestMethods, path, handler, handlers...)
|
||||||
return grp
|
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 {
|
func (grp *Group) Group(prefix string, handlers ...Handler) Router {
|
||||||
prefix = getGroupPath(grp.Prefix, prefix)
|
prefix = getGroupPath(grp.Prefix, prefix)
|
||||||
if len(handlers) > 0 {
|
if len(handlers) > 0 {
|
||||||
grp.app.register([]string{methodUse}, prefix, grp, nil, handlers...)
|
grp.app.register([]string{methodUse}, prefix, grp, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new group
|
// Create new group
|
||||||
|
|
39
helpers.go
39
helpers.go
|
@ -192,12 +192,10 @@ func (app *App) methodExistCustom(c CustomCtx) bool {
|
||||||
// uniqueRouteStack drop all not unique routes from the slice
|
// uniqueRouteStack drop all not unique routes from the slice
|
||||||
func uniqueRouteStack(stack []*Route) []*Route {
|
func uniqueRouteStack(stack []*Route) []*Route {
|
||||||
var unique []*Route
|
var unique []*Route
|
||||||
m := make(map[*Route]int)
|
m := make(map[*Route]struct{})
|
||||||
for _, v := range stack {
|
for _, v := range stack {
|
||||||
if _, ok := m[v]; !ok {
|
if _, ok := m[v]; !ok {
|
||||||
// Unique key found. Record position and collect
|
m[v] = struct{}{}
|
||||||
// in result.
|
|
||||||
m[v] = len(unique)
|
|
||||||
unique = append(unique, v)
|
unique = append(unique, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -323,28 +321,23 @@ func getSplicedStrList(headerValue string, dst []string) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
dst = dst[:0]
|
||||||
index int
|
segmentStart := 0
|
||||||
character rune
|
isLeadingSpace := true
|
||||||
lastElementEndsAt int
|
for i, c := range headerValue {
|
||||||
insertIndex int
|
switch {
|
||||||
)
|
case c == ',':
|
||||||
for index, character = range headerValue + "$" {
|
dst = append(dst, headerValue[segmentStart:i])
|
||||||
if character == ',' || index == len(headerValue) {
|
segmentStart = i + 1
|
||||||
if insertIndex >= len(dst) {
|
isLeadingSpace = true
|
||||||
oldSlice := dst
|
case c == ' ' && isLeadingSpace:
|
||||||
dst = make([]string, len(dst)+(len(dst)>>1)+2)
|
segmentStart = i + 1
|
||||||
copy(dst, oldSlice)
|
default:
|
||||||
}
|
isLeadingSpace = false
|
||||||
dst[insertIndex] = utils.TrimLeft(headerValue[lastElementEndsAt:index], ' ')
|
|
||||||
lastElementEndsAt = index + 1
|
|
||||||
insertIndex++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dst = append(dst, headerValue[segmentStart:])
|
||||||
|
|
||||||
if len(dst) > insertIndex {
|
|
||||||
dst = dst[:insertIndex]
|
|
||||||
}
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -303,6 +303,26 @@ func Test_Utils_GetSplicedStrList(t *testing.T) {
|
||||||
headerValue: "gzip,",
|
headerValue: "gzip,",
|
||||||
expectedList: []string{"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 {
|
for _, tc := range testCases {
|
||||||
|
|
80
hooks.go
80
hooks.go
|
@ -6,14 +6,15 @@ import (
|
||||||
|
|
||||||
// OnRouteHandler Handlers define a function to create hooks for Fiber.
|
// OnRouteHandler Handlers define a function to create hooks for Fiber.
|
||||||
type (
|
type (
|
||||||
OnRouteHandler = func(Route) error
|
OnRouteHandler = func(Route) error
|
||||||
OnNameHandler = OnRouteHandler
|
OnNameHandler = OnRouteHandler
|
||||||
OnGroupHandler = func(Group) error
|
OnGroupHandler = func(Group) error
|
||||||
OnGroupNameHandler = OnGroupHandler
|
OnGroupNameHandler = OnGroupHandler
|
||||||
OnListenHandler = func(ListenData) error
|
OnListenHandler = func(ListenData) error
|
||||||
OnShutdownHandler = func() error
|
OnPreShutdownHandler = func() error
|
||||||
OnForkHandler = func(int) error
|
OnPostShutdownHandler = func(error) error
|
||||||
OnMountHandler = func(*App) error
|
OnForkHandler = func(int) error
|
||||||
|
OnMountHandler = func(*App) error
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hooks is a struct to use it with App.
|
// Hooks is a struct to use it with App.
|
||||||
|
@ -22,14 +23,15 @@ type Hooks struct {
|
||||||
app *App
|
app *App
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
onRoute []OnRouteHandler
|
onRoute []OnRouteHandler
|
||||||
onName []OnNameHandler
|
onName []OnNameHandler
|
||||||
onGroup []OnGroupHandler
|
onGroup []OnGroupHandler
|
||||||
onGroupName []OnGroupNameHandler
|
onGroupName []OnGroupNameHandler
|
||||||
onListen []OnListenHandler
|
onListen []OnListenHandler
|
||||||
onShutdown []OnShutdownHandler
|
onPreShutdown []OnPreShutdownHandler
|
||||||
onFork []OnForkHandler
|
onPostShutdown []OnPostShutdownHandler
|
||||||
onMount []OnMountHandler
|
onFork []OnForkHandler
|
||||||
|
onMount []OnMountHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenData is a struct to use it with OnListenHandler
|
// ListenData is a struct to use it with OnListenHandler
|
||||||
|
@ -41,15 +43,16 @@ type ListenData struct {
|
||||||
|
|
||||||
func newHooks(app *App) *Hooks {
|
func newHooks(app *App) *Hooks {
|
||||||
return &Hooks{
|
return &Hooks{
|
||||||
app: app,
|
app: app,
|
||||||
onRoute: make([]OnRouteHandler, 0),
|
onRoute: make([]OnRouteHandler, 0),
|
||||||
onGroup: make([]OnGroupHandler, 0),
|
onGroup: make([]OnGroupHandler, 0),
|
||||||
onGroupName: make([]OnGroupNameHandler, 0),
|
onGroupName: make([]OnGroupNameHandler, 0),
|
||||||
onName: make([]OnNameHandler, 0),
|
onName: make([]OnNameHandler, 0),
|
||||||
onListen: make([]OnListenHandler, 0),
|
onListen: make([]OnListenHandler, 0),
|
||||||
onShutdown: make([]OnShutdownHandler, 0),
|
onPreShutdown: make([]OnPreShutdownHandler, 0),
|
||||||
onFork: make([]OnForkHandler, 0),
|
onPostShutdown: make([]OnPostShutdownHandler, 0),
|
||||||
onMount: make([]OnMountHandler, 0),
|
onFork: make([]OnForkHandler, 0),
|
||||||
|
onMount: make([]OnMountHandler, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,10 +99,17 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) {
|
||||||
h.app.mutex.Unlock()
|
h.app.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnShutdown is a hook to execute user functions after Shutdown.
|
// OnPreShutdown is a hook to execute user functions before Shutdown.
|
||||||
func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) {
|
func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) {
|
||||||
h.app.mutex.Lock()
|
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()
|
h.app.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,10 +201,18 @@ func (h *Hooks) executeOnListenHooks(listenData ListenData) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hooks) executeOnShutdownHooks() {
|
func (h *Hooks) executeOnPreShutdownHooks() {
|
||||||
for _, v := range h.onShutdown {
|
for _, v := range h.onPreShutdown {
|
||||||
if err := v(); err != nil {
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -83,17 +82,14 @@ func Test_Hook_OnName(t *testing.T) {
|
||||||
func Test_Hook_OnName_Error(t *testing.T) {
|
func Test_Hook_OnName_Error(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
app := New()
|
app := New()
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
require.Equal(t, "unknown error", fmt.Sprintf("%v", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
app.Hooks().OnName(func(_ Route) error {
|
app.Hooks().OnName(func(_ Route) error {
|
||||||
return errors.New("unknown 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) {
|
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) {
|
func Test_Hook_OnGroupName_Error(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
app := New()
|
app := New()
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
require.Equal(t, "unknown error", fmt.Sprintf("%v", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
app.Hooks().OnGroupName(func(_ Group) error {
|
app.Hooks().OnGroupName(func(_ Group) error {
|
||||||
return errors.New("unknown error")
|
return errors.New("unknown error")
|
||||||
})
|
})
|
||||||
|
|
||||||
grp := app.Group("/x").Name("x.")
|
require.PanicsWithError(t, "unknown error", func() {
|
||||||
grp.Get("/test", testSimpleHandler)
|
_ = app.Group("/x").Name("x.")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Hook_OnShutdown(t *testing.T) {
|
func Test_Hook_OnPrehutdown(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
app := New()
|
app := New()
|
||||||
|
|
||||||
buf := bytebufferpool.Get()
|
buf := bytebufferpool.Get()
|
||||||
defer bytebufferpool.Put(buf)
|
defer bytebufferpool.Put(buf)
|
||||||
|
|
||||||
app.Hooks().OnShutdown(func() error {
|
app.Hooks().OnPreShutdown(func() error {
|
||||||
_, err := buf.WriteString("shutdowning")
|
_, err := buf.WriteString("pre-shutdowning")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, app.Shutdown())
|
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) {
|
func Test_Hook_OnListen(t *testing.T) {
|
||||||
|
|
28
listen.go
28
listen.go
|
@ -60,17 +60,6 @@ type ListenConfig struct {
|
||||||
// Default: nil
|
// Default: nil
|
||||||
BeforeServeFunc func(app *App) error `json:"before_serve_func"`
|
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,
|
// AutoCertManager manages TLS certificates automatically using the ACME protocol,
|
||||||
// Enables integration with Let's Encrypt or other ACME-compatible providers.
|
// Enables integration with Let's Encrypt or other ACME-compatible providers.
|
||||||
//
|
//
|
||||||
|
@ -102,7 +91,7 @@ type ListenConfig struct {
|
||||||
CertClientFile string `json:"cert_client_file"`
|
CertClientFile string `json:"cert_client_file"`
|
||||||
|
|
||||||
// When the graceful shutdown begins, use this field to set the timeout
|
// 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.
|
// Set to 0 to disable the timeout and wait indefinitely.
|
||||||
//
|
//
|
||||||
// Default: 10 * time.Second
|
// Default: 10 * time.Second
|
||||||
|
@ -136,9 +125,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig {
|
||||||
return ListenConfig{
|
return ListenConfig{
|
||||||
TLSMinVersion: tls.VersionTLS12,
|
TLSMinVersion: tls.VersionTLS12,
|
||||||
ListenerNetwork: NetworkTCP4,
|
ListenerNetwork: NetworkTCP4,
|
||||||
OnShutdownError: func(err error) {
|
|
||||||
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option
|
|
||||||
},
|
|
||||||
ShutdownTimeout: 10 * time.Second,
|
ShutdownTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,12 +134,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig {
|
||||||
cfg.ListenerNetwork = NetworkTCP4
|
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 {
|
if cfg.TLSMinVersion == 0 {
|
||||||
cfg.TLSMinVersion = tls.VersionTLS12
|
cfg.TLSMinVersion = tls.VersionTLS12
|
||||||
}
|
}
|
||||||
|
@ -517,11 +497,9 @@ func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg.OnShutdownError(err)
|
app.hooks.executeOnPostShutdownHooks(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if success := cfg.OnShutdownSuccess; success != nil {
|
app.hooks.executeOnPostShutdownHooks(nil)
|
||||||
success()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
223
listen_test.go
223
listen_test.go
|
@ -15,6 +15,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/utils/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
@ -37,98 +38,42 @@ func Test_Listen(t *testing.T) {
|
||||||
|
|
||||||
// go test -run Test_Listen_Graceful_Shutdown
|
// go test -run Test_Listen_Graceful_Shutdown
|
||||||
func Test_Listen_Graceful_Shutdown(t *testing.T) {
|
func Test_Listen_Graceful_Shutdown(t *testing.T) {
|
||||||
var mu sync.Mutex
|
t.Run("Basic Graceful Shutdown", func(t *testing.T) {
|
||||||
var shutdown bool
|
testGracefulShutdown(t, 0)
|
||||||
|
|
||||||
app := New()
|
|
||||||
|
|
||||||
app.Get("/", func(c Ctx) error {
|
|
||||||
return c.SendString(c.Hostname())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ln := fasthttputil.NewInmemoryListener()
|
t.Run("Shutdown With Timeout", func(t *testing.T) {
|
||||||
errs := make(chan error)
|
testGracefulShutdown(t, 500*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
go func() {
|
t.Run("Shutdown With Timeout Error", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
testGracefulShutdown(t, 1*time.Nanosecond)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// go test -run Test_Listen_Graceful_Shutdown_Timeout
|
func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) {
|
||||||
func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) {
|
t.Helper()
|
||||||
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var shutdownSuccess bool
|
var shutdown bool
|
||||||
var shutdownTimeoutError error
|
var receivedErr error
|
||||||
|
|
||||||
app := New()
|
app := New()
|
||||||
|
|
||||||
app.Get("/", func(c Ctx) error {
|
app.Get("/", func(c Ctx) error {
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
return c.SendString(c.Hostname())
|
return c.SendString(c.Hostname())
|
||||||
})
|
})
|
||||||
|
|
||||||
ln := fasthttputil.NewInmemoryListener()
|
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() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
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{
|
errs <- app.Listener(ln, ListenConfig{
|
||||||
DisableStartupMessage: true,
|
DisableStartupMessage: true,
|
||||||
GracefulContext: ctx,
|
GracefulContext: ctx,
|
||||||
ShutdownTimeout: 500 * time.Millisecond,
|
ShutdownTimeout: shutdownTimeout,
|
||||||
OnShutdownSuccess: func() {
|
|
||||||
mu.Lock()
|
|
||||||
shutdownSuccess = true
|
|
||||||
mu.Unlock()
|
|
||||||
},
|
|
||||||
OnShutdownError: func(err error) {
|
|
||||||
mu.Lock()
|
|
||||||
shutdownTimeoutError = err
|
|
||||||
mu.Unlock()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Server readiness check
|
require.Eventually(t, func() bool {
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
conn, err := ln.Dial()
|
conn, err := ln.Dial()
|
||||||
// To test a graceful shutdown timeout, do not close the connection.
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_ = conn
|
if err := conn.Close(); err != nil {
|
||||||
break
|
t.Logf("error closing connection: %v", err)
|
||||||
}
|
}
|
||||||
// Wait a bit before retrying
|
return true
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
if i == 9 {
|
|
||||||
t.Fatalf("Server did not become ready in time: %v", err)
|
|
||||||
}
|
}
|
||||||
|
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 {
|
type testCase struct {
|
||||||
ExpectedErr error
|
expectedErr error
|
||||||
ExpectedShutdownError error
|
expectedBody string
|
||||||
ExpectedBody string
|
name string
|
||||||
Time time.Duration
|
waitTime time.Duration
|
||||||
ExpectedStatusCode int
|
expectedStatusCode int
|
||||||
ExpectedShutdownSuccess bool
|
closeConnection bool
|
||||||
}{
|
}
|
||||||
|
|
||||||
|
testCases := []testCase{
|
||||||
{
|
{
|
||||||
Time: 100 * time.Millisecond,
|
name: "Server running normally",
|
||||||
ExpectedBody: "example.com",
|
waitTime: 500 * time.Millisecond,
|
||||||
ExpectedStatusCode: StatusOK,
|
expectedBody: "example.com",
|
||||||
ExpectedErr: nil,
|
expectedStatusCode: StatusOK,
|
||||||
ExpectedShutdownError: nil,
|
expectedErr: nil,
|
||||||
ExpectedShutdownSuccess: false,
|
closeConnection: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Time: 3 * time.Second,
|
name: "Server shutdown complete",
|
||||||
ExpectedBody: "",
|
waitTime: 3 * time.Second,
|
||||||
ExpectedStatusCode: StatusOK,
|
expectedBody: "",
|
||||||
ExpectedErr: fasthttputil.ErrInmemoryListenerClosed,
|
expectedStatusCode: StatusOK,
|
||||||
ExpectedShutdownError: context.DeadlineExceeded,
|
expectedErr: fasthttputil.ErrInmemoryListenerClosed,
|
||||||
ExpectedShutdownSuccess: false,
|
closeConnection: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
time.Sleep(tc.Time)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
time.Sleep(tc.waitTime)
|
||||||
|
|
||||||
req := fasthttp.AcquireRequest()
|
req := fasthttp.AcquireRequest()
|
||||||
req.SetRequestURI("http://example.com")
|
defer fasthttp.ReleaseRequest(req)
|
||||||
|
req.SetRequestURI("http://example.com")
|
||||||
|
|
||||||
client := fasthttp.HostClient{}
|
resp := fasthttp.AcquireResponse()
|
||||||
client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() }
|
defer fasthttp.ReleaseResponse(resp)
|
||||||
|
|
||||||
resp := fasthttp.AcquireResponse()
|
err := client.Do(req, resp)
|
||||||
err := client.Do(req, resp)
|
|
||||||
|
|
||||||
if err == nil {
|
if tc.expectedErr == nil {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode())
|
require.Equal(t, tc.expectedStatusCode, resp.StatusCode())
|
||||||
require.Equal(t, tc.ExpectedBody, string(resp.Body()))
|
require.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body()))
|
||||||
} else {
|
} else {
|
||||||
require.ErrorIs(t, err, tc.ExpectedErr)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
err := <-errs
|
require.True(t, shutdown)
|
||||||
require.NoError(t, err)
|
if shutdownTimeout == 1*time.Nanosecond {
|
||||||
|
require.Error(t, receivedErr)
|
||||||
|
require.ErrorIs(t, receivedErr, context.DeadlineExceeded)
|
||||||
|
}
|
||||||
|
require.NoError(t, <-errs)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,17 +35,17 @@ const (
|
||||||
noStore = "no-store"
|
noStore = "no-store"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ignoreHeaders = map[string]any{
|
var ignoreHeaders = map[string]struct{}{
|
||||||
"Connection": nil,
|
"Connection": {},
|
||||||
"Keep-Alive": nil,
|
"Keep-Alive": {},
|
||||||
"Proxy-Authenticate": nil,
|
"Proxy-Authenticate": {},
|
||||||
"Proxy-Authorization": nil,
|
"Proxy-Authorization": {},
|
||||||
"TE": nil,
|
"TE": {},
|
||||||
"Trailers": nil,
|
"Trailers": {},
|
||||||
"Transfer-Encoding": nil,
|
"Transfer-Encoding": {},
|
||||||
"Upgrade": nil,
|
"Upgrade": {},
|
||||||
"Content-Type": nil, // already stored explicitly by the cache manager
|
"Content-Type": {}, // already stored explicitly by the cache manager
|
||||||
"Content-Encoding": nil, // already stored explicitly by the cache manager
|
"Content-Encoding": {}, // already stored explicitly by the cache manager
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheableStatusCodes = map[int]bool{
|
var cacheableStatusCodes = map[int]bool{
|
||||||
|
|
|
@ -66,6 +66,7 @@ func New(root string, cfg ...Config) fiber.Handler {
|
||||||
AcceptByteRange: config.ByteRange,
|
AcceptByteRange: config.ByteRange,
|
||||||
Compress: config.Compress,
|
Compress: config.Compress,
|
||||||
CompressBrotli: config.Compress, // Brotli compression won't work without this
|
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,
|
CompressedFileSuffixes: c.App().Config().CompressedFileSuffixes,
|
||||||
CacheDuration: config.CacheDuration,
|
CacheDuration: config.CacheDuration,
|
||||||
SkipCache: config.CacheDuration < 0,
|
SkipCache: config.CacheDuration < 0,
|
||||||
|
|
4
mount.go
4
mount.go
|
@ -55,7 +55,7 @@ func (app *App) mount(prefix string, subApp *App) Router {
|
||||||
|
|
||||||
// register mounted group
|
// register mounted group
|
||||||
mountGroup := &Group{Prefix: prefix, app: subApp}
|
mountGroup := &Group{Prefix: prefix, app: subApp}
|
||||||
app.register([]string{methodUse}, prefix, mountGroup, nil)
|
app.register([]string{methodUse}, prefix, mountGroup)
|
||||||
|
|
||||||
// Execute onMount hooks
|
// Execute onMount hooks
|
||||||
if err := subApp.hooks.executeOnMountHooks(app); err != nil {
|
if err := subApp.hooks.executeOnMountHooks(app); err != nil {
|
||||||
|
@ -85,7 +85,7 @@ func (grp *Group) mount(prefix string, subApp *App) Router {
|
||||||
|
|
||||||
// register mounted group
|
// register mounted group
|
||||||
mountGroup := &Group{Prefix: groupPath, app: subApp}
|
mountGroup := &Group{Prefix: groupPath, app: subApp}
|
||||||
grp.app.register([]string{methodUse}, groupPath, mountGroup, nil)
|
grp.app.register([]string{methodUse}, groupPath, mountGroup)
|
||||||
|
|
||||||
// Execute onMount hooks
|
// Execute onMount hooks
|
||||||
if err := subApp.hooks.executeOnMountHooks(grp.app); err != nil {
|
if err := subApp.hooks.executeOnMountHooks(grp.app); err != nil {
|
||||||
|
|
66
register.go
66
register.go
|
@ -6,18 +6,18 @@ package fiber
|
||||||
|
|
||||||
// Register defines all router handle interface generate by Route().
|
// Register defines all router handle interface generate by Route().
|
||||||
type Register interface {
|
type Register interface {
|
||||||
All(handler Handler, middleware ...Handler) Register
|
All(handler Handler, handlers ...Handler) Register
|
||||||
Get(handler Handler, middleware ...Handler) Register
|
Get(handler Handler, handlers ...Handler) Register
|
||||||
Head(handler Handler, middleware ...Handler) Register
|
Head(handler Handler, handlers ...Handler) Register
|
||||||
Post(handler Handler, middleware ...Handler) Register
|
Post(handler Handler, handlers ...Handler) Register
|
||||||
Put(handler Handler, middleware ...Handler) Register
|
Put(handler Handler, handlers ...Handler) Register
|
||||||
Delete(handler Handler, middleware ...Handler) Register
|
Delete(handler Handler, handlers ...Handler) Register
|
||||||
Connect(handler Handler, middleware ...Handler) Register
|
Connect(handler Handler, handlers ...Handler) Register
|
||||||
Options(handler Handler, middleware ...Handler) Register
|
Options(handler Handler, handlers ...Handler) Register
|
||||||
Trace(handler Handler, middleware ...Handler) Register
|
Trace(handler Handler, handlers ...Handler) Register
|
||||||
Patch(handler Handler, middleware ...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
|
Route(path string) Register
|
||||||
}
|
}
|
||||||
|
@ -45,68 +45,68 @@ type Registering struct {
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc...
|
// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc...
|
||||||
func (r *Registering) All(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) All(handler Handler, handlers ...Handler) Register {
|
||||||
r.app.register([]string{methodUse}, r.path, nil, handler, middleware...)
|
r.app.register([]string{methodUse}, r.path, nil, append([]Handler{handler}, handlers...)...)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get registers a route for GET methods that requests a representation
|
// Get registers a route for GET methods that requests a representation
|
||||||
// of the specified resource. Requests using GET should only retrieve data.
|
// of the specified resource. Requests using GET should only retrieve data.
|
||||||
func (r *Registering) Get(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Get(handler Handler, handlers ...Handler) Register {
|
||||||
r.app.Add([]string{MethodGet}, r.path, handler, middleware...)
|
r.app.Add([]string{MethodGet}, r.path, handler, handlers...)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Head registers a route for HEAD methods that asks for a response identical
|
// Head registers a route for HEAD methods that asks for a response identical
|
||||||
// to that of a GET request, but without the response body.
|
// to that of a GET request, but without the response body.
|
||||||
func (r *Registering) Head(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Head(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodHead}, handler, middleware...)
|
return r.Add([]string{MethodHead}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post registers a route for POST methods that is used to submit an entity to the
|
// 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.
|
// specified resource, often causing a change in state or side effects on the server.
|
||||||
func (r *Registering) Post(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Post(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodPost}, handler, middleware...)
|
return r.Add([]string{MethodPost}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put registers a route for PUT methods that replaces all current representations
|
// Put registers a route for PUT methods that replaces all current representations
|
||||||
// of the target resource with the request payload.
|
// of the target resource with the request payload.
|
||||||
func (r *Registering) Put(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Put(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodPut}, handler, middleware...)
|
return r.Add([]string{MethodPut}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete registers a route for DELETE methods that deletes the specified resource.
|
// Delete registers a route for DELETE methods that deletes the specified resource.
|
||||||
func (r *Registering) Delete(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Delete(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodDelete}, handler, middleware...)
|
return r.Add([]string{MethodDelete}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect registers a route for CONNECT methods that establishes a tunnel to the
|
// Connect registers a route for CONNECT methods that establishes a tunnel to the
|
||||||
// server identified by the target resource.
|
// server identified by the target resource.
|
||||||
func (r *Registering) Connect(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Connect(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodConnect}, handler, middleware...)
|
return r.Add([]string{MethodConnect}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options registers a route for OPTIONS methods that is used to describe the
|
// Options registers a route for OPTIONS methods that is used to describe the
|
||||||
// communication options for the target resource.
|
// communication options for the target resource.
|
||||||
func (r *Registering) Options(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Options(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodOptions}, handler, middleware...)
|
return r.Add([]string{MethodOptions}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace registers a route for TRACE methods that performs a message loop-back
|
// Trace registers a route for TRACE methods that performs a message loop-back
|
||||||
// test along the r.Path to the target resource.
|
// test along the r.Path to the target resource.
|
||||||
func (r *Registering) Trace(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Trace(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodTrace}, handler, middleware...)
|
return r.Add([]string{MethodTrace}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch registers a route for PATCH methods that is used to apply partial
|
// Patch registers a route for PATCH methods that is used to apply partial
|
||||||
// modifications to a resource.
|
// modifications to a resource.
|
||||||
func (r *Registering) Patch(handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Patch(handler Handler, handlers ...Handler) Register {
|
||||||
return r.Add([]string{MethodPatch}, handler, middleware...)
|
return r.Add([]string{MethodPatch}, handler, handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add allows you to specify multiple HTTP methods to register a route.
|
// Add allows you to specify multiple HTTP methods to register a route.
|
||||||
func (r *Registering) Add(methods []string, handler Handler, middleware ...Handler) Register {
|
func (r *Registering) Add(methods []string, handler Handler, handlers ...Handler) Register {
|
||||||
r.app.register(methods, r.path, nil, handler, middleware...)
|
r.app.register(methods, r.path, nil, append([]Handler{handler}, handlers...)...)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
router.go
43
router.go
|
@ -20,18 +20,18 @@ import (
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Use(args ...any) Router
|
Use(args ...any) Router
|
||||||
|
|
||||||
Get(path string, handler Handler, middleware ...Handler) Router
|
Get(path string, handler Handler, handlers ...Handler) Router
|
||||||
Head(path string, handler Handler, middleware ...Handler) Router
|
Head(path string, handler Handler, handlers ...Handler) Router
|
||||||
Post(path string, handler Handler, middleware ...Handler) Router
|
Post(path string, handler Handler, handlers ...Handler) Router
|
||||||
Put(path string, handler Handler, middleware ...Handler) Router
|
Put(path string, handler Handler, handlers ...Handler) Router
|
||||||
Delete(path string, handler Handler, middleware ...Handler) Router
|
Delete(path string, handler Handler, handlers ...Handler) Router
|
||||||
Connect(path string, handler Handler, middleware ...Handler) Router
|
Connect(path string, handler Handler, handlers ...Handler) Router
|
||||||
Options(path string, handler Handler, middleware ...Handler) Router
|
Options(path string, handler Handler, handlers ...Handler) Router
|
||||||
Trace(path string, handler Handler, middleware ...Handler) Router
|
Trace(path string, handler Handler, handlers ...Handler) Router
|
||||||
Patch(path string, handler Handler, middleware ...Handler) Router
|
Patch(path string, handler Handler, handlers ...Handler) Router
|
||||||
|
|
||||||
Add(methods []string, path string, handler Handler, middleware ...Handler) Router
|
Add(methods []string, path string, handler Handler, handlers ...Handler) Router
|
||||||
All(path string, handler Handler, middleware ...Handler) Router
|
All(path string, handler Handler, handlers ...Handler) Router
|
||||||
|
|
||||||
Group(prefix string, handlers ...Handler) Router
|
Group(prefix string, handlers ...Handler) Router
|
||||||
|
|
||||||
|
@ -318,10 +318,16 @@ func (*App) copyRoute(route *Route) *Route {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) register(methods []string, pathRaw string, group *Group, handler Handler, middleware ...Handler) {
|
func (app *App) register(methods []string, pathRaw string, group *Group, handlers ...Handler) {
|
||||||
handlers := middleware
|
// A regular route requires at least one ctx handler
|
||||||
if handler != nil {
|
if len(handlers) == 0 && group == nil {
|
||||||
handlers = append(handlers, handler)
|
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
|
// Precompute path normalization ONCE
|
||||||
|
@ -343,17 +349,14 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
|
||||||
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
|
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
|
||||||
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
|
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
|
||||||
|
|
||||||
|
isMount := group != nil && group.app != app
|
||||||
|
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
method = utils.ToUpper(method)
|
method = utils.ToUpper(method)
|
||||||
if method != methodUse && app.methodInt(method) == -1 {
|
if method != methodUse && app.methodInt(method) == -1 {
|
||||||
panic(fmt.Sprintf("add: invalid http method %s\n", method))
|
panic(fmt.Sprintf("add: invalid http method %s\n", method))
|
||||||
}
|
}
|
||||||
|
|
||||||
isMount := group != nil && group.app != app
|
|
||||||
if len(handlers) == 0 && !isMount {
|
|
||||||
panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw))
|
|
||||||
}
|
|
||||||
|
|
||||||
isUse := method == methodUse
|
isUse := method == methodUse
|
||||||
isStar := pathClean == "/*"
|
isStar := pathClean == "/*"
|
||||||
isRoot := pathClean == "/"
|
isRoot := pathClean == "/"
|
||||||
|
|
|
@ -7,7 +7,6 @@ package fiber
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -31,6 +30,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) {
|
func Test_Route_Match_SameLength(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -294,12 +326,22 @@ func Test_Router_Register_Missing_Handler(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
app := New()
|
app := New()
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
t.Run("No Handler", func(t *testing.T) {
|
||||||
require.Equal(t, "missing handler/middleware in route: /doe\n", fmt.Sprintf("%v", err))
|
t.Parallel()
|
||||||
}
|
|
||||||
}()
|
require.PanicsWithValue(t, "missing handler/middleware in route: /doe\n", func() {
|
||||||
app.register([]string{"USE"}, "/doe", nil, nil)
|
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) {
|
func Test_Ensure_Router_Interface_Implementation(t *testing.T) {
|
||||||
|
|
Loading…
Reference in New Issue