pull/3230/merge
Cory Koch 2025-04-03 08:23:39 +02:00 committed by GitHub
commit 3ca3eb92e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 615 additions and 31 deletions

View File

@ -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. 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"))
}
```

View File

@ -1175,6 +1175,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. 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 ### 🧠 Context
Fiber v3 introduces several new features and changes to the Ctx interface, enhancing its functionality and flexibility. Fiber v3 introduces several new features and changes to the Ctx interface, enhancing its functionality and flexibility.

120
router.go
View File

@ -337,6 +337,7 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
if pathRaw[0] != '/' { if pathRaw[0] != '/' {
pathRaw = "/" + pathRaw pathRaw = "/" + pathRaw
} }
pathPretty := pathRaw pathPretty := pathRaw
if !app.config.CaseSensitive { if !app.config.CaseSensitive {
pathPretty = utils.ToLower(pathPretty) pathPretty = utils.ToLower(pathPretty)
@ -344,11 +345,10 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
if !app.config.StrictRouting && len(pathPretty) > 1 { if !app.config.StrictRouting && len(pathPretty) > 1 {
pathPretty = utils.TrimRight(pathPretty, '/') pathPretty = utils.TrimRight(pathPretty, '/')
} }
pathClean := RemoveEscapeChar(pathPretty)
pathClean := RemoveEscapeChar(pathPretty)
parsedRaw := parseRoute(pathRaw, app.customConstraints...) parsedRaw := parseRoute(pathRaw, app.customConstraints...)
parsedPretty := parseRoute(pathPretty, app.customConstraints...) parsedPretty := parseRoute(pathPretty, app.customConstraints...)
isMount := group != nil && group.app != app isMount := group != nil && group.app != app
for _, method := range methods { for _, method := range methods {
@ -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) { func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
app.mutex.Lock() app.mutex.Lock()
defer app.mutex.Unlock() defer app.mutex.Unlock()
// Check mounted routes
var mounted bool var mounted bool
if len(isMounted) > 0 { if len(isMounted) > 0 {
mounted = isMounted[0] mounted = isMounted[0]
@ -408,19 +485,40 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
// Get unique HTTP method identifier // Get unique HTTP method identifier
m := app.methodInt(method) m := app.methodInt(method)
// prevent identically route registration // Check for an existing route with the same normalized path,
l := len(app.stack[m]) // same "use" flag, mount flag, and method.
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 { // If found, replace the old route with the new one.
preRoute := app.stack[m][l-1] for i, existing := range app.stack[m] {
preRoute.Handlers = append(preRoute.Handlers, route.Handlers...) 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 { } else {
// Increment global route position // 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.pos = atomic.AddUint32(&app.routesCount, 1)
route.Method = method route.Method = method
// Add route to the stack // Add route to the stack
app.stack[m] = append(app.stack[m], route) app.stack[m] = append(app.stack[m], route)
app.routesRefreshed = true app.routesRefreshed = true
}
// Execute onRoute hooks & change latestRoute if not adding mounted route // Execute onRoute hooks & change latestRoute if not adding mounted route
if !mounted { if !mounted {
@ -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. // 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 // 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 // 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. // 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: // 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 // https://github.com/gofiber/fiber/issues/2769#issuecomment-2227385283

View File

@ -11,6 +11,10 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"reflect"
"runtime"
"strings"
"sync"
"testing" "testing"
"github.com/gofiber/utils/v2" "github.com/gofiber/utils/v2"
@ -411,31 +415,452 @@ func Test_Router_NotFound_HTML_Inject(t *testing.T) {
require.Equal(t, "Cannot DELETE /does/not/exist&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;", string(c.Response.Body())) require.Equal(t, "Cannot DELETE /does/not/exist&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;", string(c.Response.Body()))
} }
func Test_App_Rebuild_Tree(t *testing.T) { func registerTreeManipulationRoutes(app *App, middleware ...func(Ctx) error) {
t.Parallel()
app := New()
app.Get("/test", func(c Ctx) error { app.Get("/test", func(c Ctx) error {
app.Get("/dynamically-defined", func(c Ctx) error { app.Get("/dynamically-defined", func(c Ctx) error {
return c.SendStatus(http.StatusOK) return c.SendStatus(StatusOK)
}) })
app.RebuildTree() app.RebuildTree()
return c.SendStatus(http.StatusOK) return c.SendStatus(StatusOK)
}, middleware...)
}
func verifyRequest(tb testing.TB, app *App, path string, expectedStatus int) *http.Response {
tb.Helper()
resp, err := app.Test(httptest.NewRequest(MethodGet, path, nil))
require.NoError(tb, err, "app.Test(req)")
require.Equal(tb, expectedStatus, resp.StatusCode, "Status code")
return resp
}
func verifyRouteHandlerCounts(tb testing.TB, app *App, expectedRoutesCount int) {
tb.Helper()
// this is taken from listen.go's printRoutesMessage app method
var routes []RouteMessage
for _, routeStack := range app.stack {
for _, route := range routeStack {
routeMsg := RouteMessage{
name: route.Name,
method: route.Method,
path: route.Path,
}
for _, handler := range route.Handlers {
routeMsg.handlers += runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() + " "
}
routes = append(routes, routeMsg)
}
}
for _, route := range routes {
require.Equal(tb, expectedRoutesCount, strings.Count(route.handlers, " "))
}
}
func verifyThereAreNoRoutes(tb testing.TB, app *App) {
tb.Helper()
require.Equal(tb, uint32(0), app.handlersCount)
require.Equal(tb, uint32(0), app.routesCount)
verifyRouteHandlerCounts(tb, app, 0)
}
func Test_App_Rebuild_Tree(t *testing.T) {
t.Parallel()
app := New()
registerTreeManipulationRoutes(app)
verifyRequest(t, app, "/dynamically-defined", StatusNotFound)
verifyRequest(t, app, "/test", StatusOK)
verifyRequest(t, app, "/dynamically-defined", StatusOK)
}
func Test_App_Remove_Route_A_B_Feature_Testing(t *testing.T) {
t.Parallel()
app := New()
app.Get("/api/feature-a", func(c Ctx) error {
app.RemoveRoute("/api/feature", false, MethodGet)
app.RebuildTree()
// Redefine route
app.Get("/api/feature", func(c Ctx) error {
return c.SendString("Testing feature-a")
}) })
resp, err := app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil)) app.RebuildTree()
require.NoError(t, err, "app.Test(req)") return c.SendStatus(StatusOK)
require.Equal(t, http.StatusNotFound, resp.StatusCode, "Status code") })
app.Get("/api/feature-b", func(c Ctx) error {
app.RemoveRoute("/api/feature", false, MethodGet)
app.RebuildTree()
// Redefine route
app.Get("/api/feature", func(c Ctx) error {
return c.SendString("Testing feature-b")
})
resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil)) app.RebuildTree()
require.NoError(t, err, "app.Test(req)") return c.SendStatus(StatusOK)
require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") })
resp, err = app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil)) verifyRequest(t, app, "/api/feature-a", StatusOK)
require.NoError(t, err, "app.Test(req)")
require.Equal(t, http.StatusOK, resp.StatusCode, "Status code") resp := verifyRequest(t, app, "/api/feature", StatusOK)
require.Equal(t, "Testing feature-a", resp, "Response Message")
resp = verifyRequest(t, app, "/api/feature-b", StatusOK)
require.Equal(t, "Testing feature-b", resp, "Response Message")
}
func Test_App_Remove_Route_By_Name(t *testing.T) {
t.Parallel()
app := New()
app.Get("/api/test", func(c Ctx) error {
return c.SendStatus(StatusOK)
}).Name("test")
app.RemoveRouteByName("test", false, MethodGet)
app.RebuildTree()
verifyRequest(t, app, "/test", StatusNotFound)
verifyThereAreNoRoutes(t, app)
}
func Test_App_Remove_Route_By_Name_Non_Existing_Route(t *testing.T) {
t.Parallel()
app := New()
app.RemoveRouteByName("test", false, MethodGet)
app.RebuildTree()
verifyThereAreNoRoutes(t, app)
}
func Test_App_Remove_Route_Nested(t *testing.T) {
t.Parallel()
app := New()
api := app.Group("/api")
v1 := api.Group("/v1")
v1.Get("/test", func(c Ctx) error {
return c.SendStatus(StatusOK)
})
verifyRequest(t, app, "/api/v1/test", StatusOK)
app.RemoveRoute("/api/v1/test", false, MethodGet)
verifyThereAreNoRoutes(t, app)
}
func Test_App_Remove_Route_Parameterized(t *testing.T) {
t.Parallel()
app := New()
app.Get("/test/:id", func(c Ctx) error {
return c.SendStatus(StatusOK)
})
verifyRequest(t, app, "/test/:id", StatusOK)
app.RemoveRoute("/test/:id", false, MethodGet)
verifyThereAreNoRoutes(t, app)
}
func Test_App_Remove_Route(t *testing.T) {
t.Parallel()
app := New()
app.Get("/test", func(c Ctx) error {
return c.SendStatus(StatusOK)
})
app.RemoveRoute("/test", false, MethodGet)
app.RebuildTree()
verifyRequest(t, app, "/test", StatusNotFound)
}
func Test_App_Remove_Route_Non_Existing_Route(t *testing.T) {
t.Parallel()
app := New()
app.RemoveRoute("/test", false, MethodGet, MethodHead)
app.RebuildTree()
verifyThereAreNoRoutes(t, app)
}
func Test_App_Remove_Route_Concurrent(t *testing.T) {
t.Parallel()
app := New()
// Add test route
app.Get("/test", func(c Ctx) error {
return c.SendStatus(StatusOK)
})
// Concurrently remove and add routes
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
app.RemoveRoute("/test", false, MethodGet)
app.Get("/test", func(c Ctx) error {
return c.SendStatus(StatusOK)
})
}()
}
wg.Wait()
// Verify final state
app.RebuildTree()
verifyRequest(t, app, "/test", StatusOK)
}
func Test_App_Route_Registration_Prevent_Duplicate(t *testing.T) {
t.Parallel()
app := New()
registerTreeManipulationRoutes(app)
registerTreeManipulationRoutes(app)
verifyRequest(t, app, "/dynamically-defined", StatusNotFound)
require.Equal(t, uint32(1), app.handlersCount)
verifyRequest(t, app, "/test", StatusOK)
require.Equal(t, uint32(2), app.handlersCount)
verifyRequest(t, app, "/dynamically-defined", StatusOK)
require.Equal(t, uint32(2), app.handlersCount)
verifyRequest(t, app, "/test", StatusOK)
require.Equal(t, uint32(2), app.handlersCount)
verifyRequest(t, app, "/dynamically-defined", StatusOK)
require.Equal(t, uint32(2), app.handlersCount)
require.Equal(t, uint32(2), app.routesCount)
verifyRouteHandlerCounts(t, app, 1)
}
func Test_Route_Registration_Prevent_Duplicate_With_Middleware(t *testing.T) {
t.Parallel()
app := New()
middleware := func(c Ctx) error {
return c.Next()
}
registerTreeManipulationRoutes(app, middleware)
registerTreeManipulationRoutes(app)
verifyRequest(t, app, "/dynamically-defined", StatusNotFound)
require.Equal(t, uint32(2), app.handlersCount)
verifyRequest(t, app, "/test", StatusOK)
require.Equal(t, uint32(3), app.handlersCount)
verifyRequest(t, app, "/dynamically-defined", StatusOK)
require.Equal(t, uint32(3), app.handlersCount)
verifyRequest(t, app, "/test", StatusOK)
require.Equal(t, uint32(3), app.handlersCount)
verifyRequest(t, app, "/dynamically-defined", StatusOK)
require.Equal(t, uint32(3), app.handlersCount)
require.Equal(t, uint32(2), app.routesCount)
verifyRouteHandlerCounts(t, app, 1)
}
func TestNormalizePath(t *testing.T) {
tests := []struct {
name string
path string
caseSensitive bool
strictRouting bool
expected string
}{
{
name: "Empty path",
path: "",
caseSensitive: true,
strictRouting: true,
expected: "/",
},
{
name: "No leading slash",
path: "users",
caseSensitive: true,
strictRouting: true,
expected: "/users",
},
{
name: "With trailing slash and strict routing",
path: "/users/",
caseSensitive: true,
strictRouting: true,
expected: "/users/",
},
{
name: "With trailing slash and non-strict routing",
path: "/users/",
caseSensitive: true,
strictRouting: false,
expected: "/users",
},
{
name: "Case sensitive",
path: "/Users",
caseSensitive: true,
strictRouting: true,
expected: "/Users",
},
{
name: "Case insensitive",
path: "/Users",
caseSensitive: false,
strictRouting: true,
expected: "/users",
},
{
name: "With escape characters",
path: "/users\\/profile",
caseSensitive: true,
strictRouting: true,
expected: "/users/profile",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := &App{
config: Config{
CaseSensitive: tt.caseSensitive,
StrictRouting: tt.strictRouting,
},
}
result := app.normalizePath(tt.path)
require.Equal(t, tt.expected, result)
})
}
}
func TestRemoveRoute(t *testing.T) {
app := New()
var buf strings.Builder
app.Use(func(c Ctx) error {
buf.WriteString("1")
return c.Next()
})
app.Post("/", func(c Ctx) error {
buf.WriteString("2")
return c.SendStatus(StatusOK)
})
app.Use("/test", func(c Ctx) error {
buf.WriteString("3")
return c.Next()
})
app.Get("/test", func(c Ctx) error {
buf.WriteString("4")
return c.SendStatus(StatusOK)
})
app.Post("/test", func(c Ctx) error {
buf.WriteString("5")
return c.SendStatus(StatusOK)
})
require.Equal(t, uint32(5), app.handlersCount)
require.Equal(t, uint32(21), app.routesCount)
req, err := http.NewRequest(MethodPost, "/", nil)
require.NoError(t, err)
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.Equal(t, "12", buf.String())
buf.Reset()
req, err = http.NewRequest(MethodGet, "/test", nil)
require.NoError(t, err)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.Equal(t, "134", buf.String())
buf.Reset()
app.RemoveRoute("/test", false, MethodGet)
app.RebuildTree()
require.Equal(t, uint32(4), app.handlersCount)
require.Equal(t, uint32(20), app.routesCount)
app.RemoveRoute("/test", false, MethodPost)
app.RebuildTree()
require.Equal(t, uint32(3), app.handlersCount)
require.Equal(t, uint32(19), app.routesCount)
req, err = http.NewRequest(MethodPost, "/test", nil)
require.NoError(t, err)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
require.Equal(t, "13", buf.String())
buf.Reset()
req, err = http.NewRequest(MethodGet, "/test", nil)
require.NoError(t, err)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
require.Equal(t, "13", buf.String())
buf.Reset()
app.RemoveRoute("/", false, MethodGet, MethodPost)
require.Equal(t, uint32(2), app.handlersCount)
require.Equal(t, uint32(18), app.routesCount)
req, err = http.NewRequest(MethodGet, "/", nil)
require.NoError(t, err)
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, 404, resp.StatusCode)
require.Equal(t, "1", buf.String())
app.RemoveRoute("/test", true, MethodGet, MethodPost)
require.Equal(t, uint32(2), app.handlersCount)
require.Equal(t, uint32(16), app.routesCount)
} }
////////////////////////////////////////////// //////////////////////////////////////////////