1
0
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:
Juan Calderon-Perez 2024-12-29 13:34:34 -05:00 committed by GitHub
parent 775e0a73f3
commit 845a7f8b8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 218 additions and 111 deletions

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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]
}
}

View File

@ -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
}

View File

@ -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
View File

@ -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
View File

@ -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

View File

@ -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