mirror of https://github.com/gofiber/fiber.git
✨ feature: add initial support for hooks (#1777)
* Add initial support for hooks.
* release ctx, mutex.
* Add unit tests.
* add comment lines.
* update
* update
* remove unnecessary code.
* fix race condition.
* fix gosec.
* skip error handling for onshutdown and onresponse.
* update
* separate hooks from app.go
* make hooks field private, hook struct public and Hooks() func.
* remove onreq and onres because of they can be done by middlewares.
* OnGroupName method.
* Update hooks.go
Co-authored-by: hi019 <65871571+hi019@users.noreply.github.com>
* handle errors for name and groupname
* fix tests.
* Update app.go
* use struct fields instead of map
* add multi-handler.
* add onGroup, make prefix field public on Group struct.
* Update hooks.go
* add newhooks method.
* ✨ feature: add initial support for hooks
* remove ctx from hooks.
Co-authored-by: hi019 <65871571+hi019@users.noreply.github.com>
Co-authored-by: wernerr <rene@gofiber.io>
pull/1818/head
parent
166e55eec6
commit
bd20e90e6b
63
app.go
63
app.go
|
@ -112,9 +112,13 @@ type App struct {
|
|||
getBytes func(s string) (b []byte)
|
||||
// Converts byte slice to a string
|
||||
getString func(b []byte) string
|
||||
|
||||
// Mounted and main apps
|
||||
appList map[string]*App
|
||||
// Hooks
|
||||
hooks *hooks
|
||||
// Latest route & group
|
||||
latestRoute *Route
|
||||
latestGroup *Group
|
||||
}
|
||||
|
||||
// Config is a struct holding the server settings.
|
||||
|
@ -424,14 +428,6 @@ const (
|
|||
DefaultCompressedFileSuffix = ".fiber.gz"
|
||||
)
|
||||
|
||||
// Variables for Name & GetRoute
|
||||
var latestRoute struct {
|
||||
route *Route
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var latestGroup Group
|
||||
|
||||
// DefaultErrorHandler that process return errors from handlers
|
||||
var DefaultErrorHandler = func(c *Ctx, err error) error {
|
||||
code := StatusInternalServerError
|
||||
|
@ -462,11 +458,17 @@ func New(config ...Config) *App {
|
|||
},
|
||||
},
|
||||
// Create config
|
||||
config: Config{},
|
||||
getBytes: utils.UnsafeBytes,
|
||||
getString: utils.UnsafeString,
|
||||
appList: make(map[string]*App),
|
||||
config: Config{},
|
||||
getBytes: utils.UnsafeBytes,
|
||||
getString: utils.UnsafeString,
|
||||
appList: make(map[string]*App),
|
||||
latestRoute: &Route{},
|
||||
latestGroup: &Group{},
|
||||
}
|
||||
|
||||
// Define hooks
|
||||
app.hooks = newHooks(app)
|
||||
|
||||
// Override config if provided
|
||||
if len(config) > 0 {
|
||||
app.config = config[0]
|
||||
|
@ -570,13 +572,18 @@ func (app *App) Mount(prefix string, fiber *App) Router {
|
|||
|
||||
// Assign name to specific route.
|
||||
func (app *App) Name(name string) Router {
|
||||
latestRoute.mu.Lock()
|
||||
if strings.HasPrefix(latestRoute.route.path, latestGroup.prefix) {
|
||||
latestRoute.route.Name = latestGroup.name + name
|
||||
app.mutex.Lock()
|
||||
if strings.HasPrefix(app.latestRoute.path, app.latestGroup.Prefix) {
|
||||
app.latestRoute.Name = app.latestGroup.name + name
|
||||
} else {
|
||||
latestRoute.route.Name = name
|
||||
app.latestRoute.Name = name
|
||||
}
|
||||
latestRoute.mu.Unlock()
|
||||
|
||||
if err := app.hooks.executeOnNameHooks(*app.latestRoute); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
app.mutex.Unlock()
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
@ -703,7 +710,12 @@ func (app *App) Group(prefix string, handlers ...Handler) Router {
|
|||
if len(handlers) > 0 {
|
||||
app.register(methodUse, prefix, handlers...)
|
||||
}
|
||||
return &Group{prefix: prefix, app: app}
|
||||
grp := &Group{Prefix: prefix, app: app}
|
||||
if err := app.hooks.executeOnGroupHooks(*grp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return grp
|
||||
}
|
||||
|
||||
// Route is used to define routes with a common prefix inside the common function.
|
||||
|
@ -919,6 +931,10 @@ func (app *App) HandlersCount() uint32 {
|
|||
//
|
||||
// Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0.
|
||||
func (app *App) Shutdown() error {
|
||||
if app.hooks != nil {
|
||||
defer app.hooks.executeOnShutdownHooks()
|
||||
}
|
||||
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
if app.server == nil {
|
||||
|
@ -932,6 +948,11 @@ func (app *App) Server() *fasthttp.Server {
|
|||
return app.server
|
||||
}
|
||||
|
||||
// Hooks returns the hook struct to register hooks.
|
||||
func (app *App) Hooks() *hooks {
|
||||
return app.hooks
|
||||
}
|
||||
|
||||
// Test is used for internal debugging by passing a *http.Request.
|
||||
// Timeout is optional and defaults to 1s, -1 will disable it completely.
|
||||
func (app *App) Test(req *http.Request, msTimeout ...int) (resp *http.Response, err error) {
|
||||
|
@ -1098,6 +1119,10 @@ func (app *App) serverErrorHandler(fctx *fasthttp.RequestCtx, err error) {
|
|||
|
||||
// startupProcess Is the method which executes all the necessary processes just before the start of the server.
|
||||
func (app *App) startupProcess() *App {
|
||||
if err := app.hooks.executeOnListenHooks(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.mutex.Lock()
|
||||
app.buildTree()
|
||||
app.mutex.Unlock()
|
||||
|
|
31
group.go
31
group.go
|
@ -13,9 +13,10 @@ import (
|
|||
|
||||
// Group struct
|
||||
type Group struct {
|
||||
app *App
|
||||
prefix string
|
||||
name string
|
||||
app *App
|
||||
name string
|
||||
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Mount attaches another app instance as a sub-router along a routing path.
|
||||
|
@ -23,7 +24,7 @@ type Group struct {
|
|||
// compose them as a single service using Mount.
|
||||
func (grp *Group) Mount(prefix string, fiber *App) Router {
|
||||
stack := fiber.Stack()
|
||||
groupPath := getGroupPath(grp.prefix, prefix)
|
||||
groupPath := getGroupPath(grp.Prefix, prefix)
|
||||
|
||||
for m := range stack {
|
||||
for r := range stack[m] {
|
||||
|
@ -46,13 +47,19 @@ func (grp *Group) Mount(prefix string, fiber *App) Router {
|
|||
|
||||
// Assign name to specific route.
|
||||
func (grp *Group) Name(name string) Router {
|
||||
if strings.HasPrefix(grp.prefix, latestGroup.prefix) {
|
||||
grp.name = latestGroup.name + name
|
||||
grp.app.mutex.Lock()
|
||||
if strings.HasPrefix(grp.Prefix, grp.app.latestGroup.Prefix) {
|
||||
grp.name = grp.app.latestGroup.name + name
|
||||
} else {
|
||||
grp.name = name
|
||||
}
|
||||
|
||||
latestGroup = *grp
|
||||
grp.app.latestGroup = grp
|
||||
|
||||
if err := grp.app.hooks.executeOnGroupNameHooks(*grp.app.latestGroup); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
grp.app.mutex.Unlock()
|
||||
|
||||
return grp
|
||||
}
|
||||
|
@ -84,14 +91,14 @@ func (grp *Group) Use(args ...interface{}) Router {
|
|||
panic(fmt.Sprintf("use: invalid handler %v\n", reflect.TypeOf(arg)))
|
||||
}
|
||||
}
|
||||
grp.app.register(methodUse, getGroupPath(grp.prefix, prefix), handlers...)
|
||||
grp.app.register(methodUse, getGroupPath(grp.Prefix, prefix), handlers...)
|
||||
return grp
|
||||
}
|
||||
|
||||
// Get registers a route for GET methods that requests a representation
|
||||
// of the specified resource. Requests using GET should only retrieve data.
|
||||
func (grp *Group) Get(path string, handlers ...Handler) Router {
|
||||
path = getGroupPath(grp.prefix, path)
|
||||
path = getGroupPath(grp.Prefix, path)
|
||||
return grp.app.Add(MethodHead, path, handlers...).Add(MethodGet, path, handlers...)
|
||||
}
|
||||
|
||||
|
@ -144,12 +151,12 @@ func (grp *Group) Patch(path string, handlers ...Handler) Router {
|
|||
|
||||
// Add allows you to specify a HTTP method to register a route
|
||||
func (grp *Group) Add(method, path string, handlers ...Handler) Router {
|
||||
return grp.app.register(method, getGroupPath(grp.prefix, path), handlers...)
|
||||
return grp.app.register(method, getGroupPath(grp.Prefix, path), handlers...)
|
||||
}
|
||||
|
||||
// Static will create a file server serving static files
|
||||
func (grp *Group) Static(prefix, root string, config ...Static) Router {
|
||||
return grp.app.registerStatic(getGroupPath(grp.prefix, prefix), root, config...)
|
||||
return grp.app.registerStatic(getGroupPath(grp.Prefix, prefix), root, config...)
|
||||
}
|
||||
|
||||
// All will register the handler on all HTTP methods
|
||||
|
@ -164,7 +171,7 @@ func (grp *Group) All(path string, handlers ...Handler) Router {
|
|||
// api := app.Group("/api")
|
||||
// api.Get("/users", handler)
|
||||
func (grp *Group) Group(prefix string, handlers ...Handler) Router {
|
||||
prefix = getGroupPath(grp.prefix, prefix)
|
||||
prefix = getGroupPath(grp.Prefix, prefix)
|
||||
if len(handlers) > 0 {
|
||||
_ = grp.app.register(methodUse, prefix, handlers...)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
package fiber
|
||||
|
||||
// Handlers define a function to create hooks for Fiber.
|
||||
type OnRouteHandler = func(Route) error
|
||||
type OnNameHandler = OnRouteHandler
|
||||
type OnGroupHandler = func(Group) error
|
||||
type OnGroupNameHandler = OnGroupHandler
|
||||
type OnListenHandler = func() error
|
||||
type OnShutdownHandler = OnListenHandler
|
||||
|
||||
type hooks struct {
|
||||
// Embed app
|
||||
app *App
|
||||
|
||||
// Hooks
|
||||
onRoute []OnRouteHandler
|
||||
onName []OnNameHandler
|
||||
onGroup []OnGroupHandler
|
||||
onGroupName []OnGroupNameHandler
|
||||
onListen []OnListenHandler
|
||||
onShutdown []OnShutdownHandler
|
||||
}
|
||||
|
||||
func newHooks(app *App) *hooks {
|
||||
return &hooks{
|
||||
app: app,
|
||||
onRoute: make([]OnRouteHandler, 0),
|
||||
onGroup: make([]OnGroupHandler, 0),
|
||||
onGroupName: make([]OnGroupNameHandler, 0),
|
||||
onName: make([]OnNameHandler, 0),
|
||||
onListen: make([]OnListenHandler, 0),
|
||||
onShutdown: make([]OnShutdownHandler, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// OnRoute is a hook to execute user functions on each route registeration.
|
||||
// Also you can get route properties by route parameter.
|
||||
func (h *hooks) OnRoute(handler ...OnRouteHandler) {
|
||||
h.app.mutex.Lock()
|
||||
h.onRoute = append(h.onRoute, handler...)
|
||||
h.app.mutex.Unlock()
|
||||
}
|
||||
|
||||
// OnName is a hook to execute user functions on each route naming.
|
||||
// Also you can get route properties by route parameter.
|
||||
//
|
||||
// WARN: OnName only works with naming routes, not groups.
|
||||
func (h *hooks) OnName(handler ...OnNameHandler) {
|
||||
h.app.mutex.Lock()
|
||||
h.onName = append(h.onName, handler...)
|
||||
h.app.mutex.Unlock()
|
||||
}
|
||||
|
||||
// OnGroup is a hook to execute user functions on each group registeration.
|
||||
// Also you can get group properties by group parameter.
|
||||
func (h *hooks) OnGroup(handler ...OnGroupHandler) {
|
||||
h.app.mutex.Lock()
|
||||
h.onGroup = append(h.onGroup, handler...)
|
||||
h.app.mutex.Unlock()
|
||||
}
|
||||
|
||||
// OnGroupName is a hook to execute user functions on each group naming.
|
||||
// Also you can get group properties by group parameter.
|
||||
//
|
||||
// WARN: OnGroupName only works with naming groups, not routes.
|
||||
func (h *hooks) OnGroupName(handler ...OnGroupNameHandler) {
|
||||
h.app.mutex.Lock()
|
||||
h.onGroupName = append(h.onGroupName, handler...)
|
||||
h.app.mutex.Unlock()
|
||||
}
|
||||
|
||||
// OnListen is a hook to execute user functions on Listen, ListenTLS, Listener.
|
||||
func (h *hooks) OnListen(handler ...OnListenHandler) {
|
||||
h.app.mutex.Lock()
|
||||
h.onListen = append(h.onListen, handler...)
|
||||
h.app.mutex.Unlock()
|
||||
}
|
||||
|
||||
// OnShutdown is a hook to execute user functions after Shutdown.
|
||||
func (h *hooks) OnShutdown(handler ...OnShutdownHandler) {
|
||||
h.app.mutex.Lock()
|
||||
h.onShutdown = append(h.onShutdown, handler...)
|
||||
h.app.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (h *hooks) executeOnRouteHooks(route Route) error {
|
||||
for _, v := range h.onRoute {
|
||||
if err := v(route); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hooks) executeOnNameHooks(route Route) error {
|
||||
|
||||
for _, v := range h.onName {
|
||||
if err := v(route); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hooks) executeOnGroupHooks(group Group) error {
|
||||
for _, v := range h.onGroup {
|
||||
if err := v(group); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hooks) executeOnGroupNameHooks(group Group) error {
|
||||
for _, v := range h.onGroupName {
|
||||
if err := v(group); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hooks) executeOnListenHooks() error {
|
||||
for _, v := range h.onListen {
|
||||
if err := v(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hooks) executeOnShutdownHooks() {
|
||||
for _, v := range h.onShutdown {
|
||||
_ = v()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package fiber
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2/internal/bytebufferpool"
|
||||
"github.com/gofiber/fiber/v2/utils"
|
||||
)
|
||||
|
||||
var testSimpleHandler = func(c *Ctx) error {
|
||||
return c.SendString("simple")
|
||||
}
|
||||
|
||||
func Test_Hook_OnRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
|
||||
app.Hooks().OnRoute(func(r Route) error {
|
||||
utils.AssertEqual(t, "", r.Name)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
app.Get("/", testSimpleHandler).Name("x")
|
||||
|
||||
subApp := New()
|
||||
subApp.Get("/test", testSimpleHandler)
|
||||
|
||||
app.Mount("/sub", subApp)
|
||||
}
|
||||
|
||||
func Test_Hook_OnName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
app.Hooks().OnName(func(r Route) error {
|
||||
buf.WriteString(r.Name)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
app.Get("/", testSimpleHandler).Name("index")
|
||||
|
||||
subApp := New()
|
||||
subApp.Get("/test", testSimpleHandler)
|
||||
subApp.Get("/test2", testSimpleHandler)
|
||||
|
||||
app.Mount("/sub", subApp)
|
||||
|
||||
utils.AssertEqual(t, "index", buf.String())
|
||||
}
|
||||
|
||||
func Test_Hook_OnName_Error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
utils.AssertEqual(t, "unknown error", fmt.Sprintf("%v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
app.Hooks().OnName(func(r Route) error {
|
||||
return errors.New("unknown error")
|
||||
})
|
||||
|
||||
app.Get("/", testSimpleHandler).Name("index")
|
||||
}
|
||||
|
||||
func Test_Hook_OnGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
app.Hooks().OnGroup(func(g Group) error {
|
||||
buf.WriteString(g.Prefix)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
grp := app.Group("/x").Name("x.")
|
||||
grp.Group("/a")
|
||||
|
||||
utils.AssertEqual(t, "/x/x/a", buf.String())
|
||||
}
|
||||
|
||||
func Test_Hook_OnGroupName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
app.Hooks().OnGroupName(func(g Group) error {
|
||||
buf.WriteString(g.name)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
grp := app.Group("/x").Name("x.")
|
||||
grp.Get("/test", testSimpleHandler)
|
||||
grp.Get("/test2", testSimpleHandler)
|
||||
|
||||
utils.AssertEqual(t, "x.", buf.String())
|
||||
}
|
||||
|
||||
func Test_Hook_OnGroupName_Error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
utils.AssertEqual(t, "unknown error", fmt.Sprintf("%v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
app.Hooks().OnGroupName(func(g Group) error {
|
||||
return errors.New("unknown error")
|
||||
})
|
||||
|
||||
grp := app.Group("/x").Name("x.")
|
||||
grp.Get("/test", testSimpleHandler)
|
||||
}
|
||||
|
||||
func Test_Hook_OnShutdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
app.Hooks().OnShutdown(func() error {
|
||||
buf.WriteString("shutdowning")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
utils.AssertEqual(t, nil, app.Shutdown())
|
||||
utils.AssertEqual(t, "shutdowning", buf.String())
|
||||
}
|
||||
|
||||
func Test_Hook_OnListen(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New(Config{
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
app.Hooks().OnListen(func() error {
|
||||
buf.WriteString("ready")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
utils.AssertEqual(t, nil, app.Shutdown())
|
||||
}()
|
||||
utils.AssertEqual(t, nil, app.Listen(":9000"))
|
||||
|
||||
utils.AssertEqual(t, "ready", buf.String())
|
||||
}
|
10
router.go
10
router.go
|
@ -167,6 +167,7 @@ func (app *App) handler(rctx *fasthttp.RequestCtx) {
|
|||
if match && app.config.ETag {
|
||||
setETag(c, false)
|
||||
}
|
||||
|
||||
// Release Ctx
|
||||
app.ReleaseCtx(c)
|
||||
}
|
||||
|
@ -435,9 +436,12 @@ func (app *App) addRoute(method string, route *Route) {
|
|||
app.routesRefreshed = true
|
||||
}
|
||||
|
||||
latestRoute.mu.Lock()
|
||||
latestRoute.route = route
|
||||
latestRoute.mu.Unlock()
|
||||
app.mutex.Lock()
|
||||
app.latestRoute = route
|
||||
if err := app.hooks.executeOnRouteHooks(*route); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
app.mutex.Unlock()
|
||||
}
|
||||
|
||||
// buildTree build the prefix tree from the previously registered routes
|
||||
|
|
Loading…
Reference in New Issue