mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-06 10:23:55 +00:00
🧹 chore: Improve Performance of Fiber Router (#3261)
* Initial improvements * Update test * Improve RemoveEscapeChar performance * Fix lint issues * Re-add comments * Add dedicated request handlers * Fix lint issues * Add test case for app.All with custom method * Add test for custom Ctx and Request Methods * Simplify test logic * Simplify test
This commit is contained in:
parent
775e0a73f3
commit
845a7f8b8e
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
@ -37,4 +37,4 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# NOTE: Keep this in sync with the version from .golangci.yml
|
||||
version: v1.62.0
|
||||
version: v1.62.2
|
||||
|
2
Makefile
2
Makefile
@ -35,7 +35,7 @@ markdown:
|
||||
## lint: 🚨 Run lint checks
|
||||
.PHONY: lint
|
||||
lint:
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 run ./...
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 run ./...
|
||||
|
||||
## test: 🚦 Execute all tests
|
||||
.PHONY: test
|
||||
|
16
app.go
16
app.go
@ -616,6 +616,10 @@ func (app *App) handleTrustedProxy(ipAddress string) {
|
||||
// Note: It doesn't allow adding new methods, only customizing exist methods.
|
||||
func (app *App) NewCtxFunc(function func(app *App) CustomCtx) {
|
||||
app.newCtxFunc = function
|
||||
|
||||
if app.server != nil {
|
||||
app.server.Handler = app.customRequestHandler
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCustomConstraint allows to register custom constraint.
|
||||
@ -868,7 +872,11 @@ func (app *App) Config() Config {
|
||||
func (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476
|
||||
// prepare the server for the start
|
||||
app.startupProcess()
|
||||
return app.requestHandler
|
||||
|
||||
if app.newCtxFunc != nil {
|
||||
return app.customRequestHandler
|
||||
}
|
||||
return app.defaultRequestHandler
|
||||
}
|
||||
|
||||
// Stack returns the raw router stack.
|
||||
@ -1057,7 +1065,11 @@ func (app *App) init() *App {
|
||||
}
|
||||
|
||||
// fasthttp server settings
|
||||
app.server.Handler = app.requestHandler
|
||||
if app.newCtxFunc != nil {
|
||||
app.server.Handler = app.customRequestHandler
|
||||
} else {
|
||||
app.server.Handler = app.defaultRequestHandler
|
||||
}
|
||||
app.server.Name = app.config.ServerHeader
|
||||
app.server.Concurrency = app.config.Concurrency
|
||||
app.server.NoDefaultDate = app.config.DisableDefaultDate
|
||||
|
49
app_test.go
49
app_test.go
@ -581,32 +581,51 @@ func Test_App_Use_StrictRouting(t *testing.T) {
|
||||
|
||||
func Test_App_Add_Method_Test(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
require.Equal(t, "add: invalid http method JANE\n", fmt.Sprintf("%v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
|
||||
app := New(Config{
|
||||
RequestMethods: methods,
|
||||
})
|
||||
|
||||
app.Add([]string{"JOHN"}, "/doe", testEmptyHandler)
|
||||
app.Add([]string{"JOHN"}, "/john", testEmptyHandler)
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest("JOHN", "/john", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/john", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusMethodNotAllowed, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/john", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusNotImplemented, resp.StatusCode, "Status code")
|
||||
|
||||
// Add a new method
|
||||
require.Panics(t, func() {
|
||||
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_App_All_Method_Test(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
|
||||
app := New(Config{
|
||||
RequestMethods: methods,
|
||||
})
|
||||
|
||||
// Add a new method with All
|
||||
app.All("/doe", testEmptyHandler)
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/doe", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusMethodNotAllowed, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/doe", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusNotImplemented, resp.StatusCode, "Status code")
|
||||
|
||||
app.Add([]string{"JANE"}, "/doe", testEmptyHandler)
|
||||
// Add a new method
|
||||
require.Panics(t, func() {
|
||||
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
|
||||
})
|
||||
}
|
||||
|
||||
// go test -run Test_App_GETOnly
|
||||
|
@ -107,8 +107,7 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error {
|
||||
func parseToMap(ptr any, data map[string][]string) error {
|
||||
elem := reflect.TypeOf(ptr).Elem()
|
||||
|
||||
//nolint:exhaustive // it's not necessary to check all types
|
||||
switch elem.Kind() {
|
||||
switch elem.Kind() { //nolint:exhaustive // it's not necessary to check all types
|
||||
case reflect.Slice:
|
||||
newMap, ok := ptr.(map[string][]string)
|
||||
if !ok {
|
||||
@ -129,7 +128,6 @@ func parseToMap(ptr any, data map[string][]string) error {
|
||||
newMap[k] = ""
|
||||
continue
|
||||
}
|
||||
|
||||
newMap[k] = v[len(v)-1]
|
||||
}
|
||||
}
|
||||
|
@ -350,5 +350,8 @@ type Ctx interface {
|
||||
setIndexRoute(route int)
|
||||
setMatched(matched bool)
|
||||
setRoute(route *Route)
|
||||
// Drop closes the underlying connection without sending any response headers or body.
|
||||
// This can be useful for silently terminating client connections, such as in DDoS mitigation
|
||||
// or when blocking access to sensitive endpoints.
|
||||
Drop() error
|
||||
}
|
||||
|
29
ctx_test.go
29
ctx_test.go
@ -127,6 +127,35 @@ func Test_Ctx_CustomCtx(t *testing.T) {
|
||||
require.Equal(t, "prefix_v3", string(body))
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_CustomCtx
|
||||
func Test_Ctx_CustomCtx_and_Method(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create app with custom request methods
|
||||
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
|
||||
app := New(Config{
|
||||
RequestMethods: methods,
|
||||
})
|
||||
|
||||
// Create custom context
|
||||
app.NewCtxFunc(func(app *App) CustomCtx {
|
||||
return &customCtx{
|
||||
DefaultCtx: *NewDefaultCtx(app),
|
||||
}
|
||||
})
|
||||
|
||||
// Add route with custom method
|
||||
app.Add([]string{"JOHN"}, "/doe", testEmptyHandler)
|
||||
resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil))
|
||||
require.NoError(t, err, "app.Test(req)")
|
||||
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
|
||||
|
||||
// Add a new method
|
||||
require.Panics(t, func() {
|
||||
app.Add([]string{"JANE"}, "/jane", testEmptyHandler)
|
||||
})
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_Accepts_EmptyAccept
|
||||
func Test_Ctx_Accepts_EmptyAccept(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
12
path.go
12
path.go
@ -620,10 +620,16 @@ func GetTrimmedParam(param string) string {
|
||||
|
||||
// RemoveEscapeChar remove escape characters
|
||||
func RemoveEscapeChar(word string) string {
|
||||
if strings.IndexByte(word, escapeChar) != -1 {
|
||||
return strings.ReplaceAll(word, string(escapeChar), "")
|
||||
b := []byte(word)
|
||||
dst := 0
|
||||
for src := 0; src < len(b); src++ {
|
||||
if b[src] == '\\' {
|
||||
continue
|
||||
}
|
||||
b[dst] = b[src]
|
||||
dst++
|
||||
}
|
||||
return word
|
||||
return string(b[:dst])
|
||||
}
|
||||
|
||||
func getParamConstraintType(constraintPart string) TypeConstraint {
|
||||
|
189
router.go
189
router.go
@ -5,11 +5,11 @@
|
||||
package fiber
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gofiber/utils/v2"
|
||||
@ -65,10 +65,12 @@ type Route struct {
|
||||
|
||||
func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool {
|
||||
// root detectionPath check
|
||||
if r.root && detectionPath == "/" {
|
||||
if r.root && len(detectionPath) == 1 && detectionPath[0] == '/' {
|
||||
return true
|
||||
// '*' wildcard matches any detectionPath
|
||||
} else if r.star {
|
||||
}
|
||||
|
||||
// '*' wildcard matches any detectionPath
|
||||
if r.star {
|
||||
if len(path) > 1 {
|
||||
params[0] = path[1:]
|
||||
} else {
|
||||
@ -76,24 +78,32 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo
|
||||
}
|
||||
return true
|
||||
}
|
||||
// Does this route have parameters
|
||||
|
||||
// Does this route have parameters?
|
||||
if len(r.Params) > 0 {
|
||||
// Match params
|
||||
if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match {
|
||||
// Get params from the path detectionPath
|
||||
return match
|
||||
}
|
||||
}
|
||||
// Is this route a Middleware?
|
||||
if r.use {
|
||||
// Single slash will match or detectionPath prefix
|
||||
if r.root || strings.HasPrefix(detectionPath, r.path) {
|
||||
// Match params using precomputed routeParser
|
||||
if r.routeParser.getMatch(detectionPath, path, params, r.use) {
|
||||
return true
|
||||
}
|
||||
// Check for a simple detectionPath match
|
||||
} else if len(r.path) == len(detectionPath) && r.path == detectionPath {
|
||||
}
|
||||
|
||||
// Middleware route?
|
||||
if r.use {
|
||||
// Single slash or prefix match
|
||||
plen := len(r.path)
|
||||
if r.root {
|
||||
// If r.root is '/', it matches everything starting at '/'
|
||||
if len(detectionPath) > 0 && detectionPath[0] == '/' {
|
||||
return true
|
||||
}
|
||||
} else if len(detectionPath) >= plen && detectionPath[:plen] == r.path {
|
||||
return true
|
||||
}
|
||||
} else if len(r.path) == len(detectionPath) && detectionPath == r.path {
|
||||
// Check exact match
|
||||
return true
|
||||
}
|
||||
|
||||
// No match
|
||||
return false
|
||||
}
|
||||
@ -201,44 +211,63 @@ func (app *App) next(c *DefaultCtx) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (app *App) requestHandler(rctx *fasthttp.RequestCtx) {
|
||||
// Handler for default ctxs
|
||||
var c CustomCtx
|
||||
var ok bool
|
||||
if app.newCtxFunc != nil {
|
||||
c, ok = app.AcquireCtx(rctx).(CustomCtx)
|
||||
if !ok {
|
||||
panic(errors.New("requestHandler: failed to type-assert to CustomCtx"))
|
||||
}
|
||||
} else {
|
||||
c, ok = app.AcquireCtx(rctx).(*DefaultCtx)
|
||||
if !ok {
|
||||
panic(errors.New("requestHandler: failed to type-assert to *DefaultCtx"))
|
||||
}
|
||||
func (app *App) defaultRequestHandler(rctx *fasthttp.RequestCtx) {
|
||||
// Acquire DefaultCtx from the pool
|
||||
ctx, ok := app.AcquireCtx(rctx).(*DefaultCtx)
|
||||
if !ok {
|
||||
panic(errors.New("requestHandler: failed to type-assert to *DefaultCtx"))
|
||||
}
|
||||
defer app.ReleaseCtx(c)
|
||||
|
||||
// handle invalid http method directly
|
||||
if app.methodInt(c.Method()) == -1 {
|
||||
_ = c.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
|
||||
defer app.ReleaseCtx(ctx)
|
||||
|
||||
// Check if the HTTP method is valid
|
||||
if ctx.methodINT == -1 {
|
||||
_ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
|
||||
return
|
||||
}
|
||||
|
||||
// check flash messages
|
||||
if strings.Contains(utils.UnsafeString(c.Request().Header.RawHeaders()), FlashCookieName) {
|
||||
c.Redirect().parseAndClearFlashMessages()
|
||||
// Optional: Check flash messages
|
||||
rawHeaders := ctx.Request().Header.RawHeaders()
|
||||
if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
|
||||
ctx.Redirect().parseAndClearFlashMessages()
|
||||
}
|
||||
|
||||
// Find match in stack
|
||||
var err error
|
||||
if app.newCtxFunc != nil {
|
||||
_, err = app.nextCustom(c)
|
||||
} else {
|
||||
_, err = app.next(c.(*DefaultCtx)) //nolint:errcheck // It is fine to ignore the error here
|
||||
}
|
||||
// Attempt to match a route and execute the chain
|
||||
_, err := app.next(ctx)
|
||||
if err != nil {
|
||||
if catch := c.App().ErrorHandler(c, err); catch != nil {
|
||||
_ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here
|
||||
if catch := ctx.App().ErrorHandler(ctx, err); catch != nil {
|
||||
_ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil
|
||||
}
|
||||
// TODO: Do we need to return here?
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) customRequestHandler(rctx *fasthttp.RequestCtx) {
|
||||
// Acquire CustomCtx from the pool
|
||||
ctx, ok := app.AcquireCtx(rctx).(CustomCtx)
|
||||
if !ok {
|
||||
panic(errors.New("requestHandler: failed to type-assert to CustomCtx"))
|
||||
}
|
||||
|
||||
defer app.ReleaseCtx(ctx)
|
||||
|
||||
// Check if the HTTP method is valid
|
||||
if app.methodInt(ctx.Method()) == -1 {
|
||||
_ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
|
||||
return
|
||||
}
|
||||
|
||||
// Optional: Check flash messages
|
||||
rawHeaders := ctx.Request().Header.RawHeaders()
|
||||
if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
|
||||
ctx.Redirect().parseAndClearFlashMessages()
|
||||
}
|
||||
|
||||
// Attempt to match a route and execute the chain
|
||||
_, err := app.nextCustom(ctx)
|
||||
if err != nil {
|
||||
if catch := ctx.App().ErrorHandler(ctx, err); catch != nil {
|
||||
_ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil
|
||||
}
|
||||
// TODO: Do we need to return here?
|
||||
}
|
||||
@ -295,68 +324,56 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
|
||||
handlers = append(handlers, handler)
|
||||
}
|
||||
|
||||
// Precompute path normalization ONCE
|
||||
if pathRaw == "" {
|
||||
pathRaw = "/"
|
||||
}
|
||||
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)
|
||||
|
||||
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
|
||||
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
|
||||
|
||||
for _, method := range methods {
|
||||
// Uppercase HTTP methods
|
||||
method = utils.ToUpper(method)
|
||||
// Check if the HTTP method is valid unless it's USE
|
||||
if method != methodUse && app.methodInt(method) == -1 {
|
||||
panic(fmt.Sprintf("add: invalid http method %s\n", method))
|
||||
}
|
||||
// is mounted app
|
||||
|
||||
isMount := group != nil && group.app != app
|
||||
// A route requires atleast one ctx handler
|
||||
if len(handlers) == 0 && !isMount {
|
||||
panic(fmt.Sprintf("missing handler/middleware in route: %s\n", pathRaw))
|
||||
}
|
||||
// Cannot have an empty path
|
||||
if pathRaw == "" {
|
||||
pathRaw = "/"
|
||||
}
|
||||
// Path always start with a '/'
|
||||
if pathRaw[0] != '/' {
|
||||
pathRaw = "/" + pathRaw
|
||||
}
|
||||
// Create a stripped path in case-sensitive / trailing slashes
|
||||
pathPretty := pathRaw
|
||||
// Case-sensitive routing, all to lowercase
|
||||
if !app.config.CaseSensitive {
|
||||
pathPretty = utils.ToLower(pathPretty)
|
||||
}
|
||||
// Strict routing, remove trailing slashes
|
||||
if !app.config.StrictRouting && len(pathPretty) > 1 {
|
||||
pathPretty = utils.TrimRight(pathPretty, '/')
|
||||
}
|
||||
// Is layer a middleware?
|
||||
isUse := method == methodUse
|
||||
// Is path a direct wildcard?
|
||||
isStar := pathPretty == "/*"
|
||||
// Is path a root slash?
|
||||
isRoot := pathPretty == "/"
|
||||
// Parse path parameters
|
||||
parsedRaw := parseRoute(pathRaw, app.customConstraints...)
|
||||
parsedPretty := parseRoute(pathPretty, app.customConstraints...)
|
||||
|
||||
// Create route metadata without pointer
|
||||
isUse := method == methodUse
|
||||
isStar := pathClean == "/*"
|
||||
isRoot := pathClean == "/"
|
||||
|
||||
route := Route{
|
||||
// Router booleans
|
||||
use: isUse,
|
||||
mount: isMount,
|
||||
star: isStar,
|
||||
root: isRoot,
|
||||
|
||||
// Path data
|
||||
path: RemoveEscapeChar(pathPretty),
|
||||
path: pathClean,
|
||||
routeParser: parsedPretty,
|
||||
Params: parsedRaw.params,
|
||||
group: group,
|
||||
|
||||
// Group data
|
||||
group: group,
|
||||
|
||||
// Public data
|
||||
Path: pathRaw,
|
||||
Method: method,
|
||||
Handlers: handlers,
|
||||
}
|
||||
|
||||
// Increment global handler count
|
||||
atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) //nolint:gosec // Not a concern
|
||||
|
||||
|
@ -591,6 +591,29 @@ func Benchmark_Router_Next_Default(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// go test -benchmem -run=^$ -bench ^Benchmark_Router_Next_Default_Parallel$ github.com/gofiber/fiber/v3 -count=1
|
||||
func Benchmark_Router_Next_Default_Parallel(b *testing.B) {
|
||||
app := New()
|
||||
app.Get("/", func(_ Ctx) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
h := app.Handler()
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
fctx := &fasthttp.RequestCtx{}
|
||||
fctx.Request.Header.SetMethod(MethodGet)
|
||||
fctx.Request.SetRequestURI("/")
|
||||
|
||||
for pb.Next() {
|
||||
h(fctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4
|
||||
func Benchmark_Route_Match(b *testing.B) {
|
||||
var match bool
|
||||
|
Loading…
x
Reference in New Issue
Block a user