bon

package module
v2.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2025 License: MIT Imports: 8 Imported by: 0

README

BON

Bon - Fast HTTP Router for Go

Bon is a high-performance HTTP router for Go that uses a double array trie data structure for efficient route matching. It focuses on speed, simplicity, and zero external dependencies.

GoDoc Widget Go Report Card

Table of Contents

Features

  • High Performance: Double array trie-based routing for optimal performance
  • Zero Dependencies: Uses only Go standard library
  • Middleware Support: Flexible middleware at router, group, and route levels
  • Standard HTTP Compatible: Works with http.Handler interface
  • Flexible Routing: Static, parameter (:param), and wildcard (*) patterns
  • All HTTP Methods: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH, CONNECT, TRACE
  • File Server: Built-in static file serving with security protections
  • Context Pooling: Efficient memory usage with sync.Pool
  • Thread-Safe: Lock-free reads using atomic operations
  • Panic Recovery: Built-in recovery middleware available
  • WebSocket Ready: Full support for WebSocket connections
  • SSE Support: Server-Sent Events with proper flushing
  • HTTP/2 Push: Server push support for modern browsers

Quick Start

package main

import (
    "net/http"
    
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
)

func main() {
    r := bon.NewRouter()
    
    // Global middleware
    r.Use(middleware.Recovery())  // Panic recovery
    
    // Simple route
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, Bon!"))
    })
    
    // Route with parameter
    r.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) {
        userID := bon.URLParam(r, "id")
        w.Write([]byte("User: " + userID))
    })
    
    http.ListenAndServe(":8080", r)
}

Installation

go get github.com/nissy/bon

Route Patterns

Pattern Types and Priority

Routes are matched in the following priority order (highest to lowest):

  1. Static routes - Exact path match

    r.Get("/users/profile", handler)  // Highest priority
    r.Get("/api/v1/status", handler)
    
  2. Parameter routes - Named parameter capture

    r.Get("/users/:id", handler)      // Captures id parameter
    r.Get("/posts/:category/:slug", handler)
    
  3. Wildcard routes - Catch-all pattern

    r.Get("/files/*", handler)        // Lowest priority
    r.Get("/api/*", handler)
    
Parameter Extraction
// Single parameter
r.Get("/users/:id", func(w http.ResponseWriter, r *http.Request) {
    userID := bon.URLParam(r, "id")
    // Use userID...
})

// Multiple parameters
r.Get("/posts/:category/:id", func(w http.ResponseWriter, r *http.Request) {
    category := bon.URLParam(r, "category")
    postID := bon.URLParam(r, "id")
    // Use parameters...
})

// Unicode parameter names are supported
r.Get("/users/:name", func(w http.ResponseWriter, r *http.Request) {
    name := bon.URLParam(r, "name")
    // Use name...
})

Middleware

Middleware Execution Order

Middleware executes in the order it was added, creating a chain:

r := bon.NewRouter()

// Execution order: Recovery -> CORS -> Auth -> Handler
r.Use(middleware.Recovery())     // 1st - Catches panics
r.Use(middleware.CORS(config))   // 2nd - Handles CORS

api := r.Group("/api")
api.Use(middleware.BasicAuth(users)) // 3rd - Authenticates
api.Get("/data", handler)        // Finally, the handler
Built-in Middleware
Recovery Middleware

Catches panics and returns 500 Internal Server Error:

r.Use(middleware.Recovery())

// With custom handler
r.Use(middleware.RecoveryWithHandler(func(w http.ResponseWriter, r *http.Request, err interface{}) {
    w.WriteHeader(500)
    w.Write([]byte(fmt.Sprintf("Panic: %v", err)))
}))
CORS Middleware

Handles Cross-Origin Resource Sharing:

r.Use(middleware.CORS(middleware.AccessControlConfig{
    AllowOrigin:      "*",
    AllowCredentials: true,
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Authorization", "Content-Type"},
    ExposeHeaders:    []string{"X-Total-Count"},
    MaxAge:           86400,
}))
Basic Auth Middleware

HTTP Basic Authentication:

users := []middleware.BasicAuthUser{
    {Name: "admin", Password: "secret"},
    {Name: "user", Password: "pass123"},
}

r.Use(middleware.BasicAuth(users))
Timeout Middleware

Request timeout handling:

r.Use(middleware.Timeout(30 * time.Second))

Groups and Routes

Group - Inherits Middleware

Groups inherit middleware from their parent and prefix all routes:

r := bon.NewRouter()
r.Use(middleware.Recovery())  // Global middleware

// API group inherits Recovery
api := r.Group("/api")
api.Use(middleware.BasicAuth(users))  // Group middleware

// All routes inherit Recovery + BasicAuth
api.Get("/users", listUsers)     // GET /api/users
api.Post("/users", createUser)   // POST /api/users

// Nested group inherits all parent middleware
v1 := api.Group("/v1")
v1.Get("/posts", listPosts)      // GET /api/v1/posts (Recovery + BasicAuth)
Route - Standalone

Routes are completely independent and don't inherit any middleware:

r := bon.NewRouter()
r.Use(middleware.BasicAuth(users))  // Global middleware

// This route is NOT affected by global middleware
standalone := r.Route()
standalone.Get("/public", handler)  // No auth required

// Must explicitly add middleware if needed
webhook := r.Route()
webhook.Use(webhookMiddleware)
webhook.Post("/webhook", handler)   // Only webhook validation, no auth

HTTP Methods

All standard HTTP methods are supported:

r.Get("/users", handler)
r.Post("/users", handler)
r.Put("/users/:id", handler)
r.Delete("/users/:id", handler)
r.Head("/", handler)
r.Options("/", handler)
r.Patch("/users/:id", handler)
r.Connect("/proxy", handler)
r.Trace("/debug", handler)

// Generic method handler
r.Handle("CUSTOM", "/", handler)

File Server

Serve static files with built-in security:

// Serve files from ./public directory at /static/*
r.FileServer("/static", "./public")

// With middleware
r.FileServer("/assets", "./assets", 
    middleware.BasicAuth(users),
    middleware.CORS(corsConfig),
)

// In a group
admin := r.Group("/admin")
admin.Use(middleware.BasicAuth(adminUsers))
admin.FileServer("/files", "./admin-files")

Security features:

  • Path traversal protection (blocks .., ./, etc.)
  • Hidden file protection (blocks . prefix files)
  • Null byte protection
  • Automatic index.html serving for directories

Custom 404 Handler

r := bon.NewRouter()

// Method 1: Direct assignment
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(404)
    w.Write([]byte(`{"error":"not found"}`))
})

// Method 2: Using SetNotFound (respects middleware)
r.SetNotFound(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(404)
    w.Write([]byte("Custom 404 page"))
})

WebSocket, SSE, and HTTP/2 Push Support

Bon supports WebSocket, Server-Sent Events (SSE), and HTTP/2 Push through Go's standard interfaces. When using middleware that wraps the ResponseWriter (like the Timeout middleware), you need to access the underlying ResponseWriter through the Unwrap() method.

WebSocket Support
package main

import (
    "net/http"
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // Configure appropriately for production
    },
}

func main() {
    r := bon.NewRouter()
    r.Use(middleware.Recovery())
    
    r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
        // When using middleware that wraps ResponseWriter
        var conn *websocket.Conn
        var err error
        
        // Try direct upgrade first
        conn, err = upgrader.Upgrade(w, r, nil)
        if err != nil {
            // If failed, try through Unwrap
            if unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
                conn, err = upgrader.Upgrade(unwrapper.Unwrap(), r, nil)
            }
            if err != nil {
                http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest)
                return
            }
        }
        defer conn.Close()
        
        // Handle WebSocket connection
        for {
            messageType, p, err := conn.ReadMessage()
            if err != nil {
                break
            }
            if err := conn.WriteMessage(messageType, p); err != nil {
                break
            }
        }
    })
    
    http.ListenAndServe(":8080", r)
}
Server-Sent Events (SSE) Support
package main

import (
    "fmt"
    "net/http"
    "time"
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
)

func main() {
    r := bon.NewRouter()
    r.Use(middleware.Recovery())
    r.Use(middleware.Timeout(30 * time.Second))
    
    r.Get("/events", func(w http.ResponseWriter, r *http.Request) {
        // Set SSE headers
        w.Header().Set("Content-Type", "text/event-stream")
        w.Header().Set("Cache-Control", "no-cache")
        w.Header().Set("Connection", "keep-alive")
        
        // Get flusher
        var flusher http.Flusher
        var ok bool
        
        // Try direct cast first
        flusher, ok = w.(http.Flusher)
        if !ok {
            // Try through Unwrap
            if unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
                flusher, ok = unwrapper.Unwrap().(http.Flusher)
            }
            if !ok {
                http.Error(w, "SSE not supported", http.StatusInternalServerError)
                return
            }
        }
        
        // Send events
        ticker := time.NewTicker(1 * time.Second)
        defer ticker.Stop()
        
        for {
            select {
            case <-r.Context().Done():
                return
            case t := <-ticker.C:
                fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
                flusher.Flush()
            }
        }
    })
    
    http.ListenAndServe(":8080", r)
}
HTTP/2 Push Support
package main

import (
    "net/http"
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
)

func main() {
    r := bon.NewRouter()
    r.Use(middleware.Recovery())
    
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        // Get pusher
        var pusher http.Pusher
        var ok bool
        
        // Try direct cast first
        pusher, ok = w.(http.Pusher)
        if !ok {
            // Try through Unwrap
            if unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
                pusher, ok = unwrapper.Unwrap().(http.Pusher)
            }
        }
        
        // Push resources if available
        if pusher != nil {
            // Push CSS and JS files
            pusher.Push("/static/style.css", &http.PushOptions{
                Header: http.Header{
                    "Content-Type": []string{"text/css"},
                },
            })
            pusher.Push("/static/app.js", &http.PushOptions{
                Header: http.Header{
                    "Content-Type": []string{"application/javascript"},
                },
            })
        }
        
        // Serve main content
        w.Header().Set("Content-Type", "text/html")
        w.Write([]byte(`
            <!DOCTYPE html>
            <html>
            <head>
                <link rel="stylesheet" href="/static/style.css">
                <script src="/static/app.js"></script>
            </head>
            <body>
                <h1>Hello with HTTP/2 Push!</h1>
            </body>
            </html>
        `))
    })
    
    // Serve static files
    r.FileServer("/static", "./static")
    
    // Note: HTTP/2 requires TLS
    http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", r)
}
Using http.ResponseController (Go 1.20+)

For Go 1.20 and later, you can use http.ResponseController which automatically handles the Unwrap() method:

func sseHandler(w http.ResponseWriter, r *http.Request) {
    rc := http.NewResponseController(w)
    
    w.Header().Set("Content-Type", "text/event-stream")
    w.WriteHeader(http.StatusOK)
    
    for {
        select {
        case <-r.Context().Done():
            return
        case <-time.After(1 * time.Second):
            fmt.Fprintf(w, "data: ping\n\n")
            if err := rc.Flush(); err != nil {
                return
            }
        }
    }
}

Examples

RESTful API
package main

import (
    "encoding/json"
    "net/http"
    "time"
    
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
)

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func main() {
    r := bon.NewRouter()
    
    // Global middleware
    r.Use(middleware.Recovery())
    r.Use(middleware.CORS(middleware.AccessControlConfig{
        AllowOrigin: "*",
    }))
    
    // API routes
    api := r.Group("/api")
    api.Use(middleware.Timeout(30 * time.Second))
    
    // User routes
    api.Get("/users", listUsers)
    api.Post("/users", createUser)
    api.Get("/users/:id", getUser)
    api.Put("/users/:id", updateUser)
    api.Delete("/users/:id", deleteUser)
    
    // Nested resources
    api.Get("/users/:userId/posts", getUserPosts)
    api.Post("/users/:userId/posts", createUserPost)
    
    http.ListenAndServe(":8080", r)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    userID := bon.URLParam(r, "id")
    user := User{ID: userID, Name: "John Doe"}
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}
API Versioning
package main

import (
    "net/http"
    "time"
    
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
)

func main() {
    r := bon.NewRouter()
    
    // API v1
    v1 := r.Group("/api/v1")
    v1.Use(middleware.CORS(middleware.AccessControlConfig{
        AllowOrigin: "*",
    }))
    
    v1.Get("/users", v1ListUsers)
    v1.Get("/posts", v1ListPosts)
    
    // API v2 with additional features
    v2 := r.Group("/api/v2")
    v2.Use(middleware.CORS(middleware.AccessControlConfig{
        AllowOrigin: "*",
    }))
    v2.Use(middleware.Timeout(30 * time.Second))
    
    v2.Get("/users", v2ListUsers)     // New response format
    v2.Get("/posts", v2ListPosts)     // Additional fields
    v2.Get("/comments", v2ListComments) // New endpoint
    
    // Health check (version independent)
    r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })
    
    http.ListenAndServe(":8080", r)
}
Authentication Example
package main

import (
    "net/http"
    
    "github.com/nissy/bon"
    "github.com/nissy/bon/middleware"
)

func main() {
    r := bon.NewRouter()
    
    // Public endpoints
    r.Get("/", homeHandler)
    r.Get("/login", loginPageHandler)
    r.Post("/login", loginHandler)
    
    // Protected API
    api := r.Group("/api")
    api.Use(middleware.BasicAuth([]middleware.BasicAuthUser{
        {Name: "user", Password: "pass"},
    }))
    
    api.Get("/profile", profileHandler)
    api.Get("/settings", settingsHandler)
    
    // Admin area with different auth
    admin := r.Group("/admin")
    admin.Use(middleware.BasicAuth([]middleware.BasicAuthUser{
        {Name: "admin", Password: "admin123"},
    }))
    
    admin.Get("/users", listAllUsers)
    admin.Delete("/users/:id", deleteUser)
    
    // Webhooks - no auth but standalone
    webhooks := r.Route()
    webhooks.Post("/webhook/github", githubWebhook)
    webhooks.Post("/webhook/stripe", stripeWebhook)
    
    http.ListenAndServe(":8080", r)
}

API Documentation

For detailed API documentation, see pkg.go.dev/github.com/nissy/bon.

Performance Tips

  1. Route Registration: Order doesn't matter - the router automatically optimizes
  2. Middleware Placement: Apply at the appropriate level for best performance
  3. Static Routes: Use exact paths when possible for fastest matching
  4. Parameter Reuse: The router pools context objects automatically

Requirements

  • Go 1.18 or higher

Testing

# Run all tests
go test ./...

# Run tests with race detection
go test -race ./...

# Run benchmarks
go test -bench=. ./...

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func URLParam

func URLParam(r *http.Request, key string) string

Types

type Context

type Context struct {
	// contains filtered or unexported fields
}

func (*Context) GetParam

func (ctx *Context) GetParam(key string) string

func (*Context) PutParam

func (ctx *Context) PutParam(key, value string)

func (*Context) WithContext

func (ctx *Context) WithContext(r *http.Request) *http.Request

allocate

type Group

type Group struct {
	// contains filtered or unexported fields
}

func (*Group) Connect

func (g *Group) Connect(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Delete

func (g *Group) Delete(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) FileServer

func (g *Group) FileServer(pattern, root string, middlewares ...Middleware)

func (*Group) Get

func (g *Group) Get(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Group

func (g *Group) Group(pattern string, middlewares ...Middleware) *Group

func (*Group) Handle

func (g *Group) Handle(method, pattern string, handler http.Handler, middlewares ...Middleware)

func (*Group) Head

func (g *Group) Head(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Options

func (g *Group) Options(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Patch

func (g *Group) Patch(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Post

func (g *Group) Post(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Put

func (g *Group) Put(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Route

func (g *Group) Route(middlewares ...Middleware) *Route

func (*Group) Trace

func (g *Group) Trace(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Group) Use

func (g *Group) Use(middlewares ...Middleware)

type Middleware

type Middleware func(http.Handler) http.Handler

type Mux

type Mux struct {
	NotFound http.HandlerFunc // 404 handler
	// contains filtered or unexported fields
}

Mux is the main HTTP router structure

func NewRouter

func NewRouter() *Mux

func (*Mux) Connect

func (m *Mux) Connect(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Delete

func (m *Mux) Delete(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) FileServer

func (m *Mux) FileServer(pattern, root string, middlewares ...Middleware)

func (*Mux) Get

func (m *Mux) Get(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Group

func (m *Mux) Group(pattern string, middlewares ...Middleware) *Group

func (*Mux) Handle

func (m *Mux) Handle(method, pattern string, handler http.Handler, middlewares ...Middleware)

func (*Mux) Head

func (m *Mux) Head(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Options

func (m *Mux) Options(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Patch

func (m *Mux) Patch(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Post

func (m *Mux) Post(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Put

func (m *Mux) Put(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Route

func (m *Mux) Route(middlewares ...Middleware) *Route

func (*Mux) ServeHTTP

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Mux) SetNotFound

func (m *Mux) SetNotFound(handler http.HandlerFunc)

SetNotFound sets custom 404 handler and rebuilds middleware chain

func (*Mux) Trace

func (m *Mux) Trace(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Mux) Use

func (m *Mux) Use(middlewares ...Middleware)

type Route

type Route struct {
	// contains filtered or unexported fields
}

func (*Route) Connect

func (r *Route) Connect(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Delete

func (r *Route) Delete(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) FileServer

func (r *Route) FileServer(pattern, root string, middlewares ...Middleware)

func (*Route) Get

func (r *Route) Get(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Group

func (r *Route) Group(pattern string, middlewares ...Middleware) *Group

func (*Route) Handle

func (r *Route) Handle(method, pattern string, handler http.Handler, middlewares ...Middleware)

func (*Route) Head

func (r *Route) Head(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Options

func (r *Route) Options(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Patch

func (r *Route) Patch(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Post

func (r *Route) Post(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Put

func (r *Route) Put(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Route

func (r *Route) Route(middlewares ...Middleware) *Route

func (*Route) Trace

func (r *Route) Trace(pattern string, handlerFunc http.HandlerFunc, middlewares ...Middleware)

func (*Route) Use

func (r *Route) Use(middlewares ...Middleware)

type Router

type Router interface {
	Handle(method, pattern string, handler http.Handler, middlewares ...Middleware)
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL