Rewrite for fasthttp

This commit is contained in:
Sky Johnson 2025-04-26 15:40:32 -05:00
parent d2e380e8a6
commit 21fe890260
6 changed files with 1491 additions and 1 deletions

17
LICENSE Normal file
View File

@ -0,0 +1,17 @@
# Sharkk Minimal License
Copyright © 2025 Sharkk, sharkk.net
You can freely use and modify this software if you follow these simple rules:
1. **Share-Alike**: When sharing modified versions, use this same license.
2. **Share Source Code**: When distributing this software, make the source code available.
3. **No Attribution Needed**: You don't need to credit the original authors publicly, but all copyright notices within the source code must remain intact.
4. **Patent Protection**: Contributors won't sue you for patent infringement on this software.
5. **No Warranty**: This software has no guarantees. You use it at your own risk.
6. **No Liability**: The authors aren't responsible for any damages or problems that might happen when you use this software.

126
README.md
View File

@ -1,3 +1,127 @@
# FastRouter
A FastHTTP-compatible version of Go/Router!
A high-performance router for fasthttp with support for path parameters, wildcards, middleware, and route grouping. Built on a prefix tree data structure with minimal allocations—🔥*blazingly* fast🔥.
## Features
- Built for fasthttp
- Zero dependencies beyond fasthttp
- Fast path matching with radix tree structure
- Path parameters (`[id]`) and wildcards (`*path`) support
- Middleware support for processing pipelines
- Route grouping with shared prefixes and middleware
- Up to 15x faster than standard routers for dynamic routes
- Zero allocations for static routes
## Installation
```shell
go get git.sharkk.net/Go/FastRouter
```
## Usage
### Basic Routing
```go
// Create a new router
r := router.New()
// Static routes
r.Get("/", func(ctx router.Ctx, params []string) {
fmt.Fprintf(ctx, "Root handler")
})
// Parameter routes
r.Get("/users/[id]", func(ctx router.Ctx, params []string) {
userID := params[0]
fmt.Fprintf(ctx, "User ID: %s", userID)
})
// Wildcard routes
r.Get("/files/*path", func(ctx router.Ctx, params []string) {
filePath := params[0]
fmt.Fprintf(ctx, "File path: %s", filePath)
})
// Standard fasthttp handler adapter
r.Get("/simple", router.StandardHandler(func(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Simple handler without params")
}))
// Start server
fasthttp.ListenAndServe(":8080", r.Handler())
```
### Middleware
```go
// Create logging middleware
func LoggingMiddleware(next router.Handler) router.Handler {
return router.newHandler(func(ctx router.Ctx, params []string) {
fmt.Println("Request started")
next.Serve(ctx, params)
fmt.Println("Request completed")
})
}
// Apply middleware globally
r := router.New()
r.Use(LoggingMiddleware)
// Apply middleware to specific routes
r.WithMiddleware(AuthMiddleware).Get("/admin", adminHandler)
```
### Route Groups
```go
// Create a router
r := router.New()
// Create an API group
api := r.Group("/api")
api.Get("/users", listUsersHandler) // matches /api/users
// Nested groups
v1 := api.Group("/v1")
v1.Get("/products", listProductsHandler) // matches /api/v1/products
// Group with middleware
admin := api.Group("/admin")
admin.Use(AuthMiddleware)
admin.Get("/stats", statsHandler) // matches /api/admin/stats
```
## Benchmarks
Benchmark comparing FastRouter to standard routers:
```
cpu: AMD Ryzen 9 7950X 16-Core Processor
BenchmarkComparison/root_path
Router: 2.098 ns/op 0 B/op 0 allocs/op
ServeMux: 32.010 ns/op 0 B/op 0 allocs/op
BenchmarkComparison/static_path
Router: 16.050 ns/op 0 B/op 0 allocs/op
ServeMux: 67.980 ns/op 0 B/op 0 allocs/op
BenchmarkComparison/dynamic_path
Router: 39.170 ns/op 16 B/op 1 allocs/op
ServeMux: 174.000 ns/op 48 B/op 3 allocs/op
BenchmarkComparison/not_found
Router: 10.580 ns/op 0 B/op 0 allocs/op
ServeMux: 178.100 ns/op 56 B/op 3 allocs/op
```
Key Performance Points:
- Root path lookups are 15x faster
- Static paths are 4x faster with zero allocations
- Dynamic paths are 4.4x faster with fewer allocations
- Not found paths are 16.8x faster with zero allocations
## License
[Sharkk Minimal License](LICENSE); do what you like!

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.sharkk.net/Go/FastRouter
go 1.24.1
require (
git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3
github.com/valyala/fasthttp v1.61.0
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3 h1:zkadtphuR4rYrKqTKZlBfbJw9wtkhSIi5extZwbx1BY=
git.sharkk.net/Go/Assert v0.0.0-20250215225259-e80b22c45aa3/go.mod h1:7AMVm0RCtLlQfWsnKs6h/IdSfzj52/o0nR03rCW68gM=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU=
github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=

489
router.go Normal file
View File

@ -0,0 +1,489 @@
package router
import (
"fmt"
"github.com/valyala/fasthttp"
)
// Ctx is an alias for fasthttp.RequestCtx for shorter, cleaner code
type Ctx = *fasthttp.RequestCtx
// Handler is an interface for handling HTTP requests with path parameters.
type Handler interface {
Serve(ctx Ctx, params []string)
}
// Middleware wraps a handler with additional functionality.
type Middleware func(Handler) Handler
// node represents a segment in the URL path and its handling logic.
type node struct {
segment string // the path segment this node matches
handler Handler // handler for this path, if it's an endpoint
children []*node // child nodes for subsequent path segments
isDynamic bool // true for param segments like [id]
isWildcard bool // true for catch-all segments like *filepath
maxParams uint8 // maximum number of parameters in paths under this node
}
// Router routes HTTP requests by method and path.
// It supports static paths, path parameters, wildcards, and middleware.
type Router struct {
get *node
post *node
put *node
patch *node
delete *node
middleware []Middleware // Global middleware
}
// Group represents a route group with a path prefix and shared middleware.
type Group struct {
router *Router
prefix string
middleware []Middleware
}
// New creates a new Router instance.
func New() *Router {
return &Router{
get: &node{},
post: &node{},
put: &node{},
patch: &node{},
delete: &node{},
middleware: []Middleware{},
}
}
// ServeHTTP implements the Handler interface for fasthttp
func (r *Router) ServeHTTP(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
method := string(ctx.Method())
handler, params, found := r.Lookup(method, path)
if !found {
ctx.SetStatusCode(fasthttp.StatusNotFound)
return
}
handler.Serve(ctx, params)
}
// Handler returns a fasthttp request handler
func (r *Router) Handler() fasthttp.RequestHandler {
return r.ServeHTTP
}
// simpleHandler implements the Handler interface
type simpleHandler struct {
fn func(ctx Ctx, params []string)
}
// Serve executes the handler function with params
func (h *simpleHandler) Serve(ctx Ctx, params []string) {
h.fn(ctx, params)
}
// Use adds middleware to the router's global middleware stack.
func (r *Router) Use(middleware ...Middleware) *Router {
r.middleware = append(r.middleware, middleware...)
return r
}
// Group creates a new route group with the given path prefix.
func (r *Router) Group(prefix string) *Group {
return &Group{
router: r,
prefix: prefix,
middleware: []Middleware{},
}
}
// Use adds middleware to the group's middleware stack.
func (g *Group) Use(middleware ...Middleware) *Group {
g.middleware = append(g.middleware, middleware...)
return g
}
// Group creates a nested group with an additional prefix.
func (g *Group) Group(prefix string) *Group {
return &Group{
router: g.router,
prefix: g.prefix + prefix,
middleware: append([]Middleware{}, g.middleware...),
}
}
// applyMiddleware wraps a handler with middleware in reverse order.
func applyMiddleware(handler Handler, middleware []Middleware) Handler {
h := handler
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
// HandlerFunc is a function that handles HTTP requests with parameters.
type HandlerFunc func(ctx Ctx, params []string)
// Handle registers a handler for the given method and path.
func (r *Router) Handle(method, path string, handler HandlerFunc) error {
root := r.methodNode(method)
if root == nil {
return fmt.Errorf("unsupported method: %s", method)
}
return r.addRoute(root, path, &simpleHandler{fn: handler}, r.middleware)
}
// methodNode returns the root node for the given HTTP method.
func (r *Router) methodNode(method string) *node {
switch method {
case "GET":
return r.get
case "POST":
return r.post
case "PUT":
return r.put
case "PATCH":
return r.patch
case "DELETE":
return r.delete
default:
return nil
}
}
// Get registers a handler for GET requests at the given path.
func (r *Router) Get(path string, handler HandlerFunc) error {
return r.Handle("GET", path, handler)
}
// Post registers a handler for POST requests at the given path.
func (r *Router) Post(path string, handler HandlerFunc) error {
return r.Handle("POST", path, handler)
}
// Put registers a handler for PUT requests at the given path.
func (r *Router) Put(path string, handler HandlerFunc) error {
return r.Handle("PUT", path, handler)
}
// Patch registers a handler for PATCH requests at the given path.
func (r *Router) Patch(path string, handler HandlerFunc) error {
return r.Handle("PATCH", path, handler)
}
// Delete registers a handler for DELETE requests at the given path.
func (r *Router) Delete(path string, handler HandlerFunc) error {
return r.Handle("DELETE", path, handler)
}
// buildGroupMiddleware returns combined middleware for the group
func (g *Group) buildGroupMiddleware() []Middleware {
middleware := append([]Middleware{}, g.router.middleware...)
return append(middleware, g.middleware...)
}
// Handle registers a handler for the given method and path.
func (g *Group) Handle(method, path string, handler HandlerFunc) error {
root := g.router.methodNode(method)
if root == nil {
return fmt.Errorf("unsupported method: %s", method)
}
fullPath := g.prefix + path
return g.router.addRoute(root, fullPath, &simpleHandler{fn: handler}, g.buildGroupMiddleware())
}
// Get registers a handler for GET requests at the given path.
func (g *Group) Get(path string, handler HandlerFunc) error {
return g.Handle("GET", path, handler)
}
// Post registers a handler for POST requests at the given path.
func (g *Group) Post(path string, handler HandlerFunc) error {
return g.Handle("POST", path, handler)
}
// Put registers a handler for PUT requests at the given path.
func (g *Group) Put(path string, handler HandlerFunc) error {
return g.Handle("PUT", path, handler)
}
// Patch registers a handler for PATCH requests at the given path.
func (g *Group) Patch(path string, handler HandlerFunc) error {
return g.Handle("PATCH", path, handler)
}
// Delete registers a handler for DELETE requests at the given path.
func (g *Group) Delete(path string, handler HandlerFunc) error {
return g.Handle("DELETE", path, handler)
}
// WithMiddleware applies specific middleware to the next route registration.
func (r *Router) WithMiddleware(middleware ...Middleware) *MiddlewareRouter {
return &MiddlewareRouter{
router: r,
middleware: middleware,
}
}
// WithMiddleware applies specific middleware to the next route registration.
func (g *Group) WithMiddleware(middleware ...Middleware) *MiddlewareGroup {
return &MiddlewareGroup{
group: g,
middleware: middleware,
}
}
// MiddlewareRouter handles route registration with specific middleware.
type MiddlewareRouter struct {
router *Router
middleware []Middleware
}
// MiddlewareGroup handles group route registration with specific middleware.
type MiddlewareGroup struct {
group *Group
middleware []Middleware
}
// buildMiddleware returns combined middleware for the middleware router
func (mr *MiddlewareRouter) buildMiddleware() []Middleware {
middleware := append([]Middleware{}, mr.router.middleware...)
return append(middleware, mr.middleware...)
}
// Handle registers a handler for the given method and path.
func (mr *MiddlewareRouter) Handle(method, path string, handler HandlerFunc) error {
root := mr.router.methodNode(method)
if root == nil {
return fmt.Errorf("unsupported method: %s", method)
}
return mr.router.addRoute(root, path, &simpleHandler{fn: handler}, mr.buildMiddleware())
}
// Get registers a handler for GET requests with specific middleware.
func (mr *MiddlewareRouter) Get(path string, handler HandlerFunc) error {
return mr.Handle("GET", path, handler)
}
// Post registers a handler for POST requests with specific middleware.
func (mr *MiddlewareRouter) Post(path string, handler HandlerFunc) error {
return mr.Handle("POST", path, handler)
}
// Put registers a handler for PUT requests with specific middleware.
func (mr *MiddlewareRouter) Put(path string, handler HandlerFunc) error {
return mr.Handle("PUT", path, handler)
}
// Patch registers a handler for PATCH requests with specific middleware.
func (mr *MiddlewareRouter) Patch(path string, handler HandlerFunc) error {
return mr.Handle("PATCH", path, handler)
}
// Delete registers a handler for DELETE requests with specific middleware.
func (mr *MiddlewareRouter) Delete(path string, handler HandlerFunc) error {
return mr.Handle("DELETE", path, handler)
}
// buildMiddleware returns combined middleware for the middleware group
func (mg *MiddlewareGroup) buildMiddleware() []Middleware {
middleware := append([]Middleware{}, mg.group.router.middleware...)
middleware = append(middleware, mg.group.middleware...)
return append(middleware, mg.middleware...)
}
// Handle registers a handler for the given method and path.
func (mg *MiddlewareGroup) Handle(method, path string, handler HandlerFunc) error {
root := mg.group.router.methodNode(method)
if root == nil {
return fmt.Errorf("unsupported method: %s", method)
}
fullPath := mg.group.prefix + path
return mg.group.router.addRoute(root, fullPath, &simpleHandler{fn: handler}, mg.buildMiddleware())
}
// Get registers a handler for GET requests with specific middleware.
func (mg *MiddlewareGroup) Get(path string, handler HandlerFunc) error {
return mg.Handle("GET", path, handler)
}
// Post registers a handler for POST requests with specific middleware.
func (mg *MiddlewareGroup) Post(path string, handler HandlerFunc) error {
return mg.Handle("POST", path, handler)
}
// Put registers a handler for PUT requests with specific middleware.
func (mg *MiddlewareGroup) Put(path string, handler HandlerFunc) error {
return mg.Handle("PUT", path, handler)
}
// Patch registers a handler for PATCH requests with specific middleware.
func (mg *MiddlewareGroup) Patch(path string, handler HandlerFunc) error {
return mg.Handle("PATCH", path, handler)
}
// Delete registers a handler for DELETE requests with specific middleware.
func (mg *MiddlewareGroup) Delete(path string, handler HandlerFunc) error {
return mg.Handle("DELETE", path, handler)
}
// StandardHandler adapts a standard fasthttp.RequestHandler to the router's HandlerFunc
func StandardHandler(handler fasthttp.RequestHandler) HandlerFunc {
return func(ctx Ctx, _ []string) {
handler(ctx)
}
}
// readSegment extracts the next path segment starting at the given position.
// Returns the segment, the position after it, and whether there are more segments.
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
if start >= len(path) {
return "", start, false
}
if path[start] == '/' {
start++
}
if start >= len(path) {
return "", start, false
}
end = start
for end < len(path) && path[end] != '/' {
end++
}
return path[start:end], end, end < len(path)
}
// addRoute adds a new route to the prefix tree with middleware.
func (r *Router) addRoute(root *node, path string, handler Handler, middleware []Middleware) error {
wrappedHandler := applyMiddleware(handler, middleware)
if path == "/" {
root.handler = wrappedHandler
return nil
}
current := root
pos := 0
var lastWildcard bool
paramsCount := uint8(0)
for {
segment, newPos, hasMore := readSegment(path, pos)
if segment == "" {
break
}
isDynamic := len(segment) > 2 && segment[0] == '[' && segment[len(segment)-1] == ']'
isWildcard := len(segment) > 0 && segment[0] == '*'
if isWildcard {
if lastWildcard {
return fmt.Errorf("wildcard must be the last segment in the path")
}
if hasMore {
return fmt.Errorf("wildcard must be the last segment in the path")
}
lastWildcard = true
}
if isDynamic || isWildcard {
paramsCount++
}
var child *node
for _, n := range current.children {
if n.segment == segment {
child = n
break
}
}
if child == nil {
child = &node{
segment: segment,
isDynamic: isDynamic,
isWildcard: isWildcard,
}
current.children = append(current.children, child)
}
if child.maxParams < paramsCount {
child.maxParams = paramsCount
}
current = child
pos = newPos
}
current.handler = wrappedHandler
return nil
}
// Lookup finds a handler matching the given method and path.
// Returns the handler, any captured parameters, and whether a match was found.
func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
root := r.methodNode(method)
if root == nil {
return nil, nil, false
}
if path == "/" {
return root.handler, []string{}, root.handler != nil
}
params := make([]string, 0, root.maxParams)
h, found := match(root, path, 0, &params)
if !found {
return nil, nil, false
}
return h, params, true
}
// match recursively traverses the prefix tree to find a matching handler.
// It populates params with any captured path parameters or wildcard matches.
func match(current *node, path string, start int, params *[]string) (Handler, bool) {
// Check for wildcard children first
for _, child := range current.children {
if child.isWildcard {
remaining := path[start:]
if len(remaining) > 0 && remaining[0] == '/' {
remaining = remaining[1:]
}
*params = append(*params, remaining)
return child.handler, child.handler != nil
}
}
// Read current segment
segment, pos, hasMore := readSegment(path, start)
if segment == "" {
return current.handler, current.handler != nil
}
// Try to match children
for _, child := range current.children {
if child.segment == segment || child.isDynamic {
if child.isDynamic {
*params = append(*params, segment)
}
if !hasMore {
return child.handler, child.handler != nil
}
if h, found := match(child, path, pos, params); found {
return h, true
}
}
}
return nil, false
}

834
router_test.go Normal file
View File

@ -0,0 +1,834 @@
package router
import (
"net"
"testing"
assert "git.sharkk.net/Go/Assert"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttputil"
)
// simpleHandler implements the Handler interface
type testHandler struct {
fn func(ctx Ctx, params []string)
}
func (h *testHandler) Serve(ctx Ctx, params []string) {
h.fn(ctx, params)
}
// newHandler creates a simple Handler from a function
func newHandler(fn func(ctx Ctx, params []string)) Handler {
return &testHandler{fn: fn}
}
// performRequest is a helper function to test the router
func performRequest(r *Router, method, path string) (*fasthttp.RequestCtx, bool) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(method)
ctx.Request.SetRequestURI(path)
handler, params, found := r.Lookup(method, path)
if found {
handler.Serve(ctx, params)
}
return ctx, found
}
func TestRootPath(t *testing.T) {
r := New()
r.Get("/", func(ctx Ctx, params []string) {
// No-op for testing
})
_, found := performRequest(r, "GET", "/")
assert.True(t, found)
}
func TestStaticPath(t *testing.T) {
r := New()
r.Get("/users/all", func(ctx Ctx, params []string) {
// No-op for testing
})
_, found := performRequest(r, "GET", "/users/all")
assert.True(t, found)
}
func TestSingleParameter(t *testing.T) {
r := New()
called := false
r.Get("/users/[id]", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, params[0], "123")
})
_, found := performRequest(r, "GET", "/users/123")
assert.True(t, found)
assert.True(t, called)
}
func TestMultipleParameters(t *testing.T) {
r := New()
called := false
r.Get("/users/[id]/posts/[postId]", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, params[0], "123")
assert.Equal(t, params[1], "456")
})
_, found := performRequest(r, "GET", "/users/123/posts/456")
assert.True(t, found)
assert.True(t, called)
}
func TestNonExistentPath(t *testing.T) {
r := New()
r.Get("/users/[id]", func(ctx Ctx, params []string) {
// No-op for testing
})
_, found := performRequest(r, "GET", "/posts/123")
assert.False(t, found)
}
func TestWrongMethod(t *testing.T) {
r := New()
r.Get("/users/[id]", func(ctx Ctx, params []string) {
// No-op for testing
})
_, found := performRequest(r, "POST", "/users/123")
assert.False(t, found)
}
func TestTrailingSlash(t *testing.T) {
r := New()
called := false
r.Get("/users/[id]", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, params[0], "123")
})
_, found := performRequest(r, "GET", "/users/123/")
assert.True(t, found)
assert.True(t, called)
}
func TestDifferentMethods(t *testing.T) {
r := New()
handler := func(ctx Ctx, params []string) {}
r.Get("/test", handler)
r.Post("/test", handler)
r.Put("/test", handler)
r.Patch("/test", handler)
r.Delete("/test", handler)
methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
for _, method := range methods {
t.Run(method, func(t *testing.T) {
_, found := performRequest(r, method, "/test")
assert.True(t, found)
})
}
}
func TestWildcardPath(t *testing.T) {
r := New()
t.Run("simple wildcard", func(t *testing.T) {
called := false
err := r.Get("/files/*path", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, params[0], "docs/report.pdf")
})
assert.Nil(t, err)
_, found := performRequest(r, "GET", "/files/docs/report.pdf")
assert.True(t, found)
assert.True(t, called)
})
t.Run("wildcard with empty path", func(t *testing.T) {
called := false
err := r.Get("/download/*filepath", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, params[0], "")
})
assert.Nil(t, err)
_, found := performRequest(r, "GET", "/download/")
assert.True(t, found)
assert.True(t, called)
})
t.Run("wildcard with parameter", func(t *testing.T) {
called := false
err := r.Get("/users/[id]/*action", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, params[0], "123")
assert.Equal(t, params[1], "settings/profile/avatar")
})
assert.Nil(t, err)
_, found := performRequest(r, "GET", "/users/123/settings/profile/avatar")
assert.True(t, found)
assert.True(t, called)
})
t.Run("multiple wildcards not allowed", func(t *testing.T) {
err := r.Get("/api/*version/*path", func(ctx Ctx, params []string) {})
assert.NotNil(t, err)
})
t.Run("non-last wildcard not allowed", func(t *testing.T) {
err := r.Get("/api/*version/users", func(ctx Ctx, params []string) {})
assert.NotNil(t, err)
})
}
// Middleware Tests
func TestMiddleware(t *testing.T) {
t.Run("global middleware", func(t *testing.T) {
r := New()
// Track middleware execution
executed := false
r.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
executed = true
next.Serve(ctx, params)
})
})
r.Get("/test", func(ctx Ctx, params []string) {})
_, found := performRequest(r, "GET", "/test")
assert.True(t, found)
assert.True(t, executed)
})
t.Run("multiple middleware", func(t *testing.T) {
r := New()
// Track middleware execution order
order := []int{}
r.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
order = append(order, 1)
next.Serve(ctx, params)
order = append(order, 4)
})
})
r.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
order = append(order, 2)
next.Serve(ctx, params)
order = append(order, 3)
})
})
r.Get("/test", func(ctx Ctx, params []string) {
order = append(order, 0)
})
_, found := performRequest(r, "GET", "/test")
assert.True(t, found)
// Check middleware execution order (first middleware wraps second)
assert.Equal(t, len(order), 5)
assert.Equal(t, order[0], 1) // First middleware enter
assert.Equal(t, order[1], 2) // Second middleware enter
assert.Equal(t, order[2], 0) // Handler
assert.Equal(t, order[3], 3) // Second middleware exit
assert.Equal(t, order[4], 4) // First middleware exit
})
t.Run("route-specific middleware", func(t *testing.T) {
r := New()
executed := false
middleware := func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
executed = true
next.Serve(ctx, params)
})
}
r.WithMiddleware(middleware).Get("/test", func(ctx Ctx, params []string) {})
_, found := performRequest(r, "GET", "/test")
assert.True(t, found)
assert.True(t, executed)
})
}
// Group Tests
func TestGroup(t *testing.T) {
t.Run("simple group", func(t *testing.T) {
r := New()
// Create API group
api := r.Group("/api")
api.Get("/users", func(ctx Ctx, params []string) {})
_, found := performRequest(r, "GET", "/api/users")
assert.True(t, found)
})
t.Run("nested groups", func(t *testing.T) {
r := New()
// Create nested groups
api := r.Group("/api")
v1 := api.Group("/v1")
v1.Get("/users", func(ctx Ctx, params []string) {})
_, found := performRequest(r, "GET", "/api/v1/users")
assert.True(t, found)
})
t.Run("group middleware", func(t *testing.T) {
r := New()
executed := false
// Create group with middleware
api := r.Group("/api")
api.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
executed = true
next.Serve(ctx, params)
})
})
api.Get("/users", func(ctx Ctx, params []string) {})
_, found := performRequest(r, "GET", "/api/users")
assert.True(t, found)
assert.True(t, executed)
})
t.Run("nested group middleware", func(t *testing.T) {
r := New()
order := []int{}
// Create group with middleware
api := r.Group("/api")
api.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
order = append(order, 1)
next.Serve(ctx, params)
})
})
// Create nested group with additional middleware
v1 := api.Group("/v1")
v1.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
order = append(order, 2)
next.Serve(ctx, params)
})
})
v1.Get("/users", func(ctx Ctx, params []string) {
order = append(order, 3)
})
_, found := performRequest(r, "GET", "/api/v1/users")
assert.True(t, found)
// Check middleware execution order
assert.Equal(t, len(order), 3)
assert.Equal(t, order[0], 1) // First middleware (from api group)
assert.Equal(t, order[1], 2) // Second middleware (from v1 group)
assert.Equal(t, order[2], 3) // Handler
})
t.Run("route-specific middleware in group", func(t *testing.T) {
r := New()
order := []int{}
// Create group with middleware
api := r.Group("/api")
api.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
order = append(order, 1)
next.Serve(ctx, params)
})
})
// Add route with specific middleware
api.WithMiddleware(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
order = append(order, 2)
next.Serve(ctx, params)
})
}).Get("/users", func(ctx Ctx, params []string) {
order = append(order, 3)
})
_, found := performRequest(r, "GET", "/api/users")
assert.True(t, found)
// Check middleware execution order
assert.Equal(t, len(order), 3)
assert.Equal(t, order[0], 1) // Group middleware
assert.Equal(t, order[1], 2) // Route-specific middleware
assert.Equal(t, order[2], 3) // Handler
})
}
// Tests for standard handlers
func TestStandardHandlers(t *testing.T) {
r := New()
handlerCalled := false
standardHandler := func(ctx *fasthttp.RequestCtx) {
handlerCalled = true
}
r.Get("/standard", StandardHandler(standardHandler))
_, found := performRequest(r, "GET", "/standard")
assert.True(t, found)
assert.True(t, handlerCalled)
}
// Test complete HTTP handler chain
func TestHandlerChain(t *testing.T) {
r := New()
handlerCalled := false
r.Get("/complete", func(ctx Ctx, params []string) {
handlerCalled = true
})
_, found := performRequest(r, "GET", "/complete")
assert.True(t, found)
assert.True(t, handlerCalled)
}
// Test advanced routes with multiple parameters
func TestAdvancedRoutes(t *testing.T) {
r := New()
called := false
r.Get("/api/[version]/users/[id]/profiles/[profile]", func(ctx Ctx, params []string) {
called = true
assert.Equal(t, len(params), 3)
assert.Equal(t, params[0], "v1")
assert.Equal(t, params[1], "123")
assert.Equal(t, params[2], "basic")
})
_, found := performRequest(r, "GET", "/api/v1/users/123/profiles/basic")
assert.True(t, found)
assert.True(t, called)
wildcardCalled := false
r.Get("/files/[type]", func(ctx Ctx, params []string) {
wildcardCalled = true
assert.Equal(t, params[0], "pdf")
})
_, found = performRequest(r, "GET", "/files/pdf")
assert.True(t, found)
assert.True(t, wildcardCalled)
}
// Test 404 handling
func TestNotFoundHandling(t *testing.T) {
r := New()
r.Get("/exists", func(ctx Ctx, params []string) {
ctx.SetStatusCode(fasthttp.StatusOK)
})
ln := fasthttputil.NewInmemoryListener()
defer ln.Close()
go fasthttp.Serve(ln, r.Handler())
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://example.com/not-exists")
req.Header.SetMethod("GET")
client := &fasthttp.HostClient{
Addr: "example.com",
Dial: func(addr string) (net.Conn, error) {
return ln.Dial()
},
}
err := client.Do(req, resp)
assert.Nil(t, err)
assert.Equal(t, resp.StatusCode(), fasthttp.StatusNotFound)
}
// Benchmarks
func BenchmarkRouterLookup(b *testing.B) {
r := New()
handler := func(ctx Ctx, params []string) {}
// Setup routes for benchmarking
r.Get("/", handler)
r.Get("/users/all", handler)
r.Get("/users/[id]", handler)
r.Get("/users/[id]/posts/[postId]", handler)
b.Run("root", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/")
}
})
b.Run("static", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/users/all")
}
})
b.Run("single_param", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/users/123")
}
})
b.Run("multi_param", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/users/123/posts/456")
}
})
b.Run("not_found", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/nonexistent/path")
}
})
}
func BenchmarkParallelLookup(b *testing.B) {
r := New()
handler := func(ctx Ctx, params []string) {}
r.Get("/users/[id]", handler)
r.Get("/posts/[id]/comments", handler)
r.Get("/products/[category]/[id]", handler)
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
switch i % 3 {
case 0:
r.Lookup("GET", "/users/123")
case 1:
r.Lookup("GET", "/posts/456/comments")
case 2:
r.Lookup("GET", "/products/electronics/789")
}
i++
}
})
}
func BenchmarkWildcardLookup(b *testing.B) {
r := New()
handler := func(ctx Ctx, params []string) {}
// Setup routes for benchmarking
r.Get("/files/*path", handler)
r.Get("/users/[id]/*action", handler)
b.Run("simple_wildcard", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/files/documents/reports/2024/q1.pdf")
}
})
b.Run("wildcard_with_param", func(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/users/123/settings/profile/avatar")
}
})
}
func BenchmarkMiddleware(b *testing.B) {
passthrough := func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
next.Serve(ctx, params)
})
}
b.Run("no_middleware", func(b *testing.B) {
r := New()
r.Get("/test", func(ctx Ctx, params []string) {})
dummyCtx := &fasthttp.RequestCtx{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/test")
h.Serve(dummyCtx, params)
}
})
b.Run("one_middleware", func(b *testing.B) {
r := New()
r.Use(passthrough)
r.Get("/test", func(ctx Ctx, params []string) {})
dummyCtx := &fasthttp.RequestCtx{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/test")
h.Serve(dummyCtx, params)
}
})
b.Run("five_middleware", func(b *testing.B) {
r := New()
for i := 0; i < 5; i++ {
r.Use(passthrough)
}
r.Get("/test", func(ctx Ctx, params []string) {})
dummyCtx := &fasthttp.RequestCtx{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/test")
h.Serve(dummyCtx, params)
}
})
}
func BenchmarkGroups(b *testing.B) {
handler := func(ctx Ctx, params []string) {}
b.Run("flat_route", func(b *testing.B) {
r := New()
r.Get("/api/v1/users", handler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/api/v1/users")
}
})
b.Run("grouped_route", func(b *testing.B) {
r := New()
api := r.Group("/api")
v1 := api.Group("/v1")
v1.Get("/users", handler)
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Lookup("GET", "/api/v1/users")
}
})
b.Run("grouped_route_with_middleware", func(b *testing.B) {
r := New()
api := r.Group("/api")
api.Use(func(next Handler) Handler {
return newHandler(func(ctx Ctx, params []string) {
next.Serve(ctx, params)
})
})
v1 := api.Group("/v1")
v1.Get("/users", handler)
dummyCtx := &fasthttp.RequestCtx{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
h, params, _ := r.Lookup("GET", "/api/v1/users")
h.Serve(dummyCtx, params)
}
})
}
func BenchmarkFasthttpServer(b *testing.B) {
// Create a local listener
ln := fasthttputil.NewInmemoryListener()
defer ln.Close()
r := New()
r.Get("/users", func(ctx Ctx, params []string) {})
r.Get("/users/all", func(ctx Ctx, params []string) {})
r.Get("/users/[id]", func(ctx Ctx, params []string) {})
// Start the server
go fasthttp.Serve(ln, r.Handler())
// Create a client
client := &fasthttp.HostClient{
Addr: "example.com",
Dial: func(addr string) (net.Conn, error) {
return ln.Dial()
},
}
b.Run("root", func(b *testing.B) {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://example.com/")
req.Header.SetMethod("GET")
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.Do(req, resp)
}
})
b.Run("static_path", func(b *testing.B) {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://example.com/users/all")
req.Header.SetMethod("GET")
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.Do(req, resp)
}
})
b.Run("param_path", func(b *testing.B) {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI("http://example.com/users/123")
req.Header.SetMethod("GET")
b.ResetTimer()
for i := 0; i < b.N; i++ {
client.Do(req, resp)
}
})
}
func BenchmarkComparison(b *testing.B) {
// Custom router setup
customRouter := New()
handler := func(ctx Ctx, params []string) {}
customRouter.Get("/", handler)
customRouter.Get("/users/all", handler)
customRouter.Get("/users/[id]", handler)
customRouter.Get("/users/[id]/posts/[postId]", handler)
// Simple fasthttp router setup (similar to http.ServeMux)
routes := map[string]fasthttp.RequestHandler{
"/": func(ctx *fasthttp.RequestCtx) {},
"/users/all": func(ctx *fasthttp.RequestCtx) {},
"/users/": func(ctx *fasthttp.RequestCtx) {}, // Best equivalent for dynamic routes
}
muxHandler := func(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
if handler, ok := routes[path]; ok {
handler(ctx)
return
}
// Try prefix match (like http.ServeMux does)
for pattern, handler := range routes {
if len(pattern) > 0 && pattern[len(pattern)-1] == '/' && len(path) >= len(pattern) && path[:len(pattern)] == pattern {
handler(ctx)
return
}
}
ctx.SetStatusCode(fasthttp.StatusNotFound)
}
// Root path
b.Run("root_path", func(b *testing.B) {
b.Run("custom", func(b *testing.B) {
for i := 0; i < b.N; i++ {
customRouter.Lookup("GET", "/")
}
})
b.Run("mux", func(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/")
for i := 0; i < b.N; i++ {
muxHandler(ctx)
}
})
})
// Static path
b.Run("static_path", func(b *testing.B) {
b.Run("custom", func(b *testing.B) {
for i := 0; i < b.N; i++ {
customRouter.Lookup("GET", "/users/all")
}
})
b.Run("mux", func(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/users/all")
for i := 0; i < b.N; i++ {
muxHandler(ctx)
}
})
})
// Dynamic path
b.Run("dynamic_path", func(b *testing.B) {
b.Run("custom", func(b *testing.B) {
for i := 0; i < b.N; i++ {
customRouter.Lookup("GET", "/users/123")
}
})
b.Run("mux", func(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/users/123")
for i := 0; i < b.N; i++ {
muxHandler(ctx)
}
})
})
// Not found
b.Run("not_found", func(b *testing.B) {
b.Run("custom", func(b *testing.B) {
for i := 0; i < b.N; i++ {
customRouter.Lookup("GET", "/nonexistent/path")
}
})
b.Run("mux", func(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI("/nonexistent/path")
for i := 0; i < b.N; i++ {
muxHandler(ctx)
}
})
})
}