Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 feat: Add Support for Removing Routes #3230

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -761,3 +761,56 @@ func main() {
```

In this example, a new route is defined and then `RebuildTree()` is called to ensure the new route is registered and available.


## RemoveRoute

This method removes a route by path. You must call the `RebuildTree()` method after the remove in to ensure the route is removed.

```go title="Signature"
func (app *App) RemoveRoute(path string, methods ...string)
```

This method removes a route by name
```go title="Signature"
func (app *App) RemoveRouteByName(name string, methods ...string)
```

```go title="Example"
package main

import (
"log"

"github.com/gofiber/fiber/v3"
)

func main() {
app := fiber.New()

app.Get("/api/feature-a", func(c *fiber.Ctx) error {
app.RemoveRoute("/api/feature", fiber.MethodGet)
app.RebuildTree()
// Redefine route
app.Get("/api/feature", func(c *fiber.Ctx) error {
return c.SendString("Testing feature-a")
})

app.RebuildTree()
return c.SendStatus(fiber.StatusOK)
})
app.Get("/api/feature-b", func(c *fiber.Ctx) error {
app.RemoveRoute("/api/feature", fiber.MethodGet)
app.RebuildTree()
// Redefine route
app.Get("/api/feature", func(c *fiber.Ctx) error {
return c.SendString("Testing feature-b")
})

app.RebuildTree()
return c.SendStatus(fiber.StatusOK)
})

log.Fatal(app.Listen(":3000"))
}
```
8 changes: 8 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,14 @@ In this example, a new route is defined, and `RebuildTree()` is called to ensure

Note: Use this method with caution. It is **not** thread-safe and can be very performance-intensive. Therefore, it should be used sparingly and primarily in development mode. It should not be invoke concurrently.

## RemoveRoute

- **RemoveRoute**: Removes route by path

- **RemoveRouteByName**: Removes route by name

For more details, refer to the [app documentation](./api/app.md#removeroute):

ckoch786 marked this conversation as resolved.
Show resolved Hide resolved
### 🧠 Context

Fiber v3 introduces several new features and changes to the Ctx interface, enhancing its functionality and flexibility.
Expand Down
76 changes: 75 additions & 1 deletion router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"html"
"slices"
"sort"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -302,6 +303,13 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
if method != methodUse && app.methodInt(method) == -1 {
panic(fmt.Sprintf("add: invalid http method %s\n", method))
}

// Duplicate Route Handling
if app.routeExists(method, pathRaw) {
matchPathFunc := func(r *Route) bool { return r.Path == pathRaw }
app.deleteRoute([]string{method}, matchPathFunc)
}

// is mounted app
isMount := group != nil && group.app != app
// A route requires atleast one ctx handler
Expand Down Expand Up @@ -375,6 +383,72 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
}
}

func (app *App) routeExists(method, pathRaw string) bool {
pathToCheck := pathRaw
if !app.config.CaseSensitive {
pathToCheck = utils.ToLower(pathToCheck)
}

return slices.ContainsFunc(app.stack[app.methodInt(method)], func(r *Route) bool {
routePath := r.path
if !app.config.CaseSensitive {
routePath = utils.ToLower(routePath)
}

return routePath == pathToCheck
})
}

// RemoveRoute is used to remove a route from the stack by path.
// This only needs to be called to remove a route, route registration prevents duplicate routes.
// You should call RebuildTree after using this to ensure consistency of the tree.
func (app *App) RemoveRoute(path string, methods ...string) {
pathMatchFunc := func(r *Route) bool { return r.Path == path }
app.deleteRoute(methods, pathMatchFunc)
}

// RemoveRouteByName is used to remove a route from the stack by name.
// This only needs to be called to remove a route, route registration prevents duplicate routes.
// You should call RebuildTree after using this to ensure consistency of the tree.
func (app *App) RemoveRouteByName(name string, methods ...string) {
matchFunc := func(r *Route) bool { return r.Name == name }
app.deleteRoute(methods, matchFunc)
}

func (app *App) deleteRoute(methods []string, matchFunc func(r *Route) bool) {
app.mutex.Lock()
defer app.mutex.Unlock()

for _, method := range methods {
// Uppercase HTTP methods
method = utils.ToUpper(method)

// Get unique HTTP method identifier
m := app.methodInt(method)
if m == -1 {
continue // Skip invalid HTTP methods
}

// Find the index of the route to remove
index := slices.IndexFunc(app.stack[m], matchFunc)
if index == -1 {
continue // Route not found
}

route := app.stack[m][index]

// Decrement global handler count
atomic.AddUint32(&app.handlersCount, ^uint32(len(route.Handlers)-1)) //nolint:gosec // Not a concern
// Decrement global route position
atomic.AddUint32(&app.routesCount, ^uint32(0))

// Remove route from tree stack
app.stack[m] = slices.Delete(app.stack[m], index, index+1)
}

app.routesRefreshed = true
ckoch786 marked this conversation as resolved.
Show resolved Hide resolved
}

func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
app.mutex.Lock()
defer app.mutex.Unlock()
Expand Down Expand Up @@ -415,7 +489,7 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
// 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
// is only done in the startupProcess of the app, this method does not make 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
Expand Down
Loading