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 10 commits into
base: main
Choose a base branch
from
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 @@ -1174,6 +1174,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):

### 🧠 Context

Fiber v3 introduces several new features and changes to the Ctx interface, enhancing its functionality and flexibility.
Expand Down
130 changes: 114 additions & 16 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,18 +337,18 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
if pathRaw[0] != '/' {
pathRaw = "/" + pathRaw
}

pathPretty := pathRaw
if !app.config.CaseSensitive {
pathPretty = utils.ToLower(pathPretty)
}
if !app.config.StrictRouting && len(pathPretty) > 1 {
pathPretty = utils.TrimRight(pathPretty, '/')
}
pathClean := RemoveEscapeChar(pathPretty)

pathClean := RemoveEscapeChar(pathPretty)
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
parsedPretty := parseRoute(pathPretty, app.customConstraints...)

isMount := group != nil && group.app != app

for _, method := range methods {
Expand Down Expand Up @@ -395,11 +395,88 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
}
}

func (app *App) normalizePath(path string) string {
if path == "" {
path = "/"
}
if path[0] != '/' {
path = "/" + path
}
if !app.config.CaseSensitive {
path = utils.ToLower(path)
}
if !app.config.StrictRouting && len(path) > 1 {
path = utils.TrimRight(path, '/')
}
return RemoveEscapeChar(path)
}

// 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, removeMiddlewares bool, methods ...string) {
// Normalize same as register uses
norm := app.normalizePath(path)

pathMatchFunc := func(r *Route) bool {
return r.path == norm // compare private normalized path
}
app.deleteRoute(methods, removeMiddlewares, 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, removeMiddlewares bool, methods ...string) {
matchFunc := func(r *Route) bool { return r.Name == name }
app.deleteRoute(methods, removeMiddlewares, matchFunc)
}

func (app *App) deleteRoute(methods []string, removeMiddlewares bool, 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
}

for i, route := range app.stack[m] {
var removedUseHandler bool
// only remove middlewares when use is true and method is use, if not middleware just check path
if (removeMiddlewares && route.use && matchFunc(route)) || (!route.use && matchFunc(route)) {
// Remove route from stack
if i+1 < len(app.stack[m]) {
app.stack[m] = append(app.stack[m][:i], app.stack[m][i+1:]...)
} else {
app.stack[m] = app.stack[m][:i]
}
app.routesRefreshed = true

// Decrement global handler count. In middleware routes, only decrement once
if (route.use && !removedUseHandler) || !route.use {
removedUseHandler = true
atomic.AddUint32(&app.handlersCount, ^uint32(len(route.Handlers)-1)) //nolint:gosec // Not a concern
}

// Decrement global route count
atomic.AddUint32(&app.routesCount, ^uint32(0)) //nolint:gosec // Not a concern
}
}
}

app.routesRefreshed = true
}

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 {
mounted = isMounted[0]
Expand All @@ -408,20 +485,41 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
// Get unique HTTP method identifier
m := app.methodInt(method)

// prevent identically route registration
l := len(app.stack[m])
if l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use && !route.mount && !app.stack[m][l-1].mount {
preRoute := app.stack[m][l-1]
preRoute.Handlers = append(preRoute.Handlers, route.Handlers...)
} else {
// Increment global route position
route.pos = atomic.AddUint32(&app.routesCount, 1)
route.Method = method
// Add route to the stack
app.stack[m] = append(app.stack[m], route)
app.routesRefreshed = true
// Check for an existing route with the same normalized path,
// same "use" flag, mount flag, and method.
// If found, replace the old route with the new one.
for i, existing := range app.stack[m] {
if existing.path == route.path &&
existing.use == route.use &&
existing.mount == route.mount &&
existing.Method == route.Method {
if route.use { // middleware: merge handlers instead of replacing
app.stack[m][i].Handlers = append(existing.Handlers, route.Handlers...) //nolint:gocritic // Not a concern
} else {
// For non-middleware routes, replace as before
atomic.AddUint32(&app.handlersCount, ^uint32(len(existing.Handlers)-1)) //nolint:gosec // Not a concern
route.pos = existing.pos
app.stack[m][i] = route
}
app.routesRefreshed = true
if !mounted {
app.latestRoute = route
if err := app.hooks.executeOnRouteHooks(*route); err != nil {
panic(err)
}
}
return
}
}

// No duplicate route exists; add the new route normally.
route.pos = atomic.AddUint32(&app.routesCount, 1)
route.Method = method

// Add route to the stack
app.stack[m] = append(app.stack[m], route)
app.routesRefreshed = true

// Execute onRoute hooks & change latestRoute if not adding mounted route
if !mounted {
app.latestRoute = route
Expand All @@ -435,7 +533,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