Skip to content
22 changes: 22 additions & 0 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,25 @@ Hooks is a method to return [hooks](./hooks.md) property.
```go title="Signature"
func (app *App) Hooks() *Hooks
```

## RebuildTree

RebuildTree is a method destined to rebuild the route tree stack and allow dynamically route registers. It returns an app pointer.

```go title="Signature"
func (app *App) RebuildTree() *App
```

**NOTE**: This method should be used in the most careful way possible, since it's not currently possible to make it thread-safe (it would add a big performance overhead to do so) and calling it is very performance-intensive, so it's recommended to be used only in development mode and never concurrently. Here's an example of defining routes dynamically:

```go
app.Get("/define", func(c Ctx) error {
app.Get("/dynamically-defined", func(c Ctx) error {
return c.SendStatus(http.StatusOK)
})

app.RebuildTree()

return c.SendStatus(http.StatusOK)
})
```
15 changes: 15 additions & 0 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,21 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
}
}

// 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 {
Expand Down
28 changes: 28 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
Expand Down Expand Up @@ -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 /////////////////
//////////////////////////////////////////////
Expand Down