diff --git a/docs/api/app.md b/docs/api/app.md index c3ecb552f8..ef9c2ea08d 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -573,3 +573,31 @@ Hooks is a method to return [hooks](./hooks.md) property. ```go title="Signature" func (app *App) Hooks() *Hooks ``` + +## RebuildTree + +The RebuildTree method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the App instance. + +```go title="Signature" +func (app *App) RebuildTree() *App +``` + +**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in development mode. Avoid using it concurrently. + +### Example Usage + +Here’s an example of how to define and register routes dynamically: + +```go +app.Get("/define", func(c Ctx) error { // Define a new route dynamically + app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route + return c.SendStatus(http.StatusOK) + }) + + app.RebuildTree() // Rebuild the route tree to register the new route + + return c.SendStatus(http.StatusOK) +}) +``` + +In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available. diff --git a/docs/whats_new.md b/docs/whats_new.md index 9155792f6b..aa1d05a759 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -389,6 +389,31 @@ app.Route("/api").Route("/user/:id?") }); ``` +### 🗺 RebuildTree + +We have added a new method that allows the route tree stack to be rebuilt in runtime, with it, you can add a route while your application is running and rebuild the route tree stack to make it registered and available for calls. + +You can find more reference on it in the [app](./api/app.md#rebuildtree): + +#### Example Usage + +```go +app.Get("/define", func(c Ctx) error { // Define a new route dynamically + app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route + return c.SendStatus(http.StatusOK) + }) + + app.RebuildTree() // Rebuild the route tree to register the new route + + return c.SendStatus(http.StatusOK) +}) +``` + +In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available. + +**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in +development mode. Avoid using it concurrently. + ### 🧠 Context ### 📎 Parser diff --git a/router.go b/router.go index eae9adef70..bc93f67977 100644 --- a/router.go +++ b/router.go @@ -375,6 +375,9 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler } func (app *App) addRoute(method string, route *Route, isMounted ...bool) { + app.mutex.Lock() + defer app.mutex.Unlock() + // Check mounted routes var mounted bool if len(isMounted) > 0 { @@ -400,15 +403,28 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) { // Execute onRoute hooks & change latestRoute if not adding mounted route if !mounted { - app.mutex.Lock() app.latestRoute = route if err := app.hooks.executeOnRouteHooks(*route); err != nil { panic(err) } - app.mutex.Unlock() } } +// BuildTree rebuilds the prefix tree from the previously registered routes. +// This method is useful when you want to register routes dynamically after the app has started. +// It is not recommended to use this method on production environments because rebuilding +// the tree is performance-intensive and not thread-safe in runtime. Since building the tree +// is only done in the startupProcess of the app, this method does not makes sure that the +// routeTree is being safely changed, as it would add a great deal of overhead in the request. +// Latest benchmark results showed a degradation from 82.79 ns/op to 94.48 ns/op and can be found in: +// https://github.com/gofiber/fiber/issues/2769#issuecomment-2227385283 +func (app *App) RebuildTree() *App { + app.mutex.Lock() + defer app.mutex.Unlock() + + return app.buildTree() +} + // buildTree build the prefix tree from the previously registered routes func (app *App) buildTree() *App { if !app.routesRefreshed { diff --git a/router_test.go b/router_test.go index 715fbf7661..5509039c66 100644 --- a/router_test.go +++ b/router_test.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net/http" "net/http/httptest" "os" "testing" @@ -368,6 +369,33 @@ func Test_Router_NotFound_HTML_Inject(t *testing.T) { require.Equal(t, "Cannot DELETE /does/not/exist<script>alert('foo');</script>", string(c.Response.Body())) } +func Test_App_Rebuild_Tree(t *testing.T) { + t.Parallel() + app := New() + + app.Get("/test", func(c Ctx) error { + app.Get("/dynamically-defined", func(c Ctx) error { + return c.SendStatus(http.StatusOK) + }) + + app.RebuildTree() + + return c.SendStatus(http.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, http.StatusNotFound, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") +} + ////////////////////////////////////////////// ///////////////// BENCHMARKS ///////////////// //////////////////////////////////////////////