Skip to content

Commit

Permalink
🔥 feat: Add Support for Removing Routes (gofiber#3230)
Browse files Browse the repository at this point in the history
* Add new methods named RemoveRoute and RemoveRouteByName.

* Update register method to prevent duplicate routes.

* Clean up tests

* Update docs

* Add Dockerfile
  • Loading branch information
ckoch786 committed Dec 14, 2024
1 parent a63bd34 commit 6909630
Show file tree
Hide file tree
Showing 5 changed files with 405 additions and 11 deletions.
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Use the official Golang image to create a build artifact.
# This is based on Debian and sets the GOPATH to /go.
FROM golang:latest AS builder

ARG TARGETOS
ARG TARGETARCH

# Create and change to the app directory.
WORKDIR /app

# Copy local code to the container image.
COPY . ./

# Install dependencies and tidy up the go.mod and go.sum files.
RUN go mod tidy

# Build the binary.
# -mod=readonly ensures immutable go.mod and go.sum in container builds.
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -mod=readonly -v -o server
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()
```

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

```go title="Example"
package main

import (
"log"

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

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

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

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

app.RebuildTree()
return c.SendStatus(http.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 @@ -941,6 +941,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
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 string, 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
}

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

0 comments on commit 6909630

Please sign in to comment.