Skip to content

Latest commit

History

121 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

BON

Bon - Fast HTTP Router for Go

Bon is a high-performance HTTP router for Go that uses exact-match indexing, segment tree routing, and a Patricia-style fallback for efficient route matching. It focuses on speed, simplicity, and zero external dependencies.

GoDoc WidgetGo Report Card

Table of Contents

Features

  • High Performance: Static fast path and segment tree-based dynamic routing
  • 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: Recovery middleware available
  • WebSocket Ready: Works with standard WebSocket upgrade flows
  • SSE Support: Server-Sent Events with proper flushing
  • HTTP/2 Push: Supports http.Pusher when the underlying server provides it

Quick Start

package main
import (
"net/http""github.com/nissy/bon/v2""github.com/nissy/bon/v2/middleware"
)
funcmain() {
r:=bon.NewRouter()
// Global middlewarer.Use(middleware.Recovery()) // Panic recovery// Simple router.Get("/", func(w http.ResponseWriter, r*http.Request) {
w.Write([]byte("Hello, Bon!"))
})
// Route with parameterr.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/v2

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 priorityr.Get("/api/v1/status", handler)
  2. Parameter routes - Named parameter capture

    r.Get("/users/:id", handler) // Captures id parameterr.Get("/posts/:category/:slug", handler)
    r.Get("/api/v:version/users", handler) // Captures version from v1, v2, etc.
  3. Wildcard routes - Catch-all or single-segment wildcard pattern

    r.Get("/files/*", handler) // Matches the remaining pathr.Get("/api/*", handler)
    r.Get("/files/*/metadata", handler) // * matches one segment before /metadata

Parameter names are read with bon.URLParam(r, "name"). Wildcard values are not exposed through URLParam.

Parameter Extraction

// Single parameterr.Get("/users/:id", func(w http.ResponseWriter, r*http.Request) {
userID:=bon.URLParam(r, "id")
// Use userID...
})
// Multiple parametersr.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 supportedr.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 -> Handlerr.Use(middleware.Recovery()) // 1st - Catches panicsr.Use(middleware.CORS(config)) // 2nd - Handles CORSapi:=r.Group("/api")
api.Use(middleware.BasicAuth(users)) // 3rd - Authenticatesapi.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 handlerr.Use(middleware.RecoveryWithHandler(func(w http.ResponseWriter, r*http.Request, errinterface{}) {
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 - Prefix and Group Middleware

Groups prefix all routes and inherit middleware from parent groups. Global middleware registered with r.Use(...) is applied to every route at the router level.

r:=bon.NewRouter()
r.Use(middleware.Recovery()) // Global middleware applied to every routeapi:=r.Group("/api")
api.Use(middleware.BasicAuth(users)) // Group middleware// These routes use global Recovery + group BasicAuthapi.Get("/users", listUsers) // GET /api/usersapi.Post("/users", createUser) // POST /api/users// Nested group inherits parent group middlewarev1:=api.Group("/v1")
v1.Get("/posts", listPosts) // GET /api/v1/posts

Route - Standalone From Group Middleware

Routes created with Route() do not inherit group middleware. Global middleware registered with r.Use(...) still applies.

r:=bon.NewRouter()
r.Use(middleware.Recovery()) // Global middleware still appliesapi:=r.Group("/api")
api.Use(middleware.BasicAuth(users))
api.Get("/private", handler) // Uses Recovery + BasicAuth// This route keeps the /api prefix but does not inherit BasicAuthpublic:=api.Route()
public.Get("/public", handler) // Uses Recovery only// Must explicitly add middleware if neededwebhook:=api.Route()
webhook.Use(webhookMiddleware)
webhook.Post("/webhook", handler) // Uses Recovery + webhookMiddleware

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 handlerr.Handle("CUSTOM", "/", handler)

File Server

Serve static files with built-in security:

// Serve files from ./public directory at /static/*r.FileServer("/static", "./public")
// With middlewarer.FileServer("/assets", "./assets", middleware.BasicAuth(users),
middleware.CORS(corsConfig),
)
// In a groupadmin:=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()
// SetNotFound rebuilds the internal middleware chain for 404 responses.r.SetNotFound(func(w http.ResponseWriter, r*http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(404)
w.Write([]byte(`{"error":"not found"}`))
})

WebSocket, SSE, and HTTP/2 Push Support

Bon works with 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), access the underlying ResponseWriter through the Unwrap() method when needed.

WebSocket Support

package main
import (
"net/http""github.com/nissy/bon/v2""github.com/nissy/bon/v2/middleware""github.com/gorilla/websocket"
)
varupgrader= websocket.Upgrader{
CheckOrigin: func(r*http.Request) bool {
returntrue// Configure appropriately for production
},
}
funcmain() {
r:=bon.NewRouter()
r.Use(middleware.Recovery())
r.Get("/ws", func(w http.ResponseWriter, r*http.Request) {
// When using middleware that wraps ResponseWritervarconn*websocket.Connvarerrerror// Try direct upgrade firstconn, err=upgrader.Upgrade(w, r, nil)
iferr!=nil {
// If failed, try through Unwrapifunwrapper, ok:=w.(interface{ Unwrap() http.ResponseWriter }); ok {
conn, err=upgrader.Upgrade(unwrapper.Unwrap(), r, nil)
}
iferr!=nil {
http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest)
return
}
}
deferconn.Close()
// Handle WebSocket connectionfor {
messageType, p, err:=conn.ReadMessage()
iferr!=nil {
break
}
iferr:=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/v2""github.com/nissy/bon/v2/middleware"
)
funcmain() {
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 headersw.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// Get flushervarflusher http.Flushervarokbool// Try direct cast firstflusher, ok=w.(http.Flusher)
if!ok {
// Try through Unwrapifunwrapper, 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 eventsticker:=time.NewTicker(1*time.Second)
deferticker.Stop()
for {
select {
case<-r.Context().Done():
returncaset:=<-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/v2""github.com/nissy/bon/v2/middleware"
)
funcmain() {
r:=bon.NewRouter()
r.Use(middleware.Recovery())
r.Get("/", func(w http.ResponseWriter, r*http.Request) {
// Get pushervarpusher http.Pushervarokbool// Try direct cast firstpusher, ok=w.(http.Pusher)
if!ok {
// Try through Unwrapifunwrapper, ok:=w.(interface{ Unwrap() http.ResponseWriter }); ok {
pusher, ok=unwrapper.Unwrap().(http.Pusher)
}
}
// Push resources if availableifpusher!=nil {
// Push CSS and JS filespusher.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 contentw.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 filesr.FileServer("/static", "./static")
// Note: HTTP/2 requires TLShttp.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:

funcsseHandler(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():
returncase<-time.After(1*time.Second):
fmt.Fprintf(w, "data: ping\n\n")
iferr:=rc.Flush(); err!=nil {
return
}
}
}
}

Examples

RESTful API

package main
import (
"encoding/json""net/http""time""github.com/nissy/bon/v2""github.com/nissy/bon/v2/middleware"
)
typeUserstruct {
IDstring`json:"id"`Namestring`json:"name"`
}
funcmain() {
r:=bon.NewRouter()
// Global middlewarer.Use(middleware.Recovery())
r.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
}))
// API routesapi:=r.Group("/api")
api.Use(middleware.Timeout(30*time.Second))
// User routesapi.Get("/users", listUsers)
api.Post("/users", createUser)
api.Get("/users/:id", getUser)
api.Put("/users/:id", updateUser)
api.Delete("/users/:id", deleteUser)
// Nested resourcesapi.Get("/users/:userId/posts", getUserPosts)
api.Post("/users/:userId/posts", createUserPost)
http.ListenAndServe(":8080", r)
}
funcgetUser(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/v2""github.com/nissy/bon/v2/middleware"
)
funcmain() {
r:=bon.NewRouter()
// API v1v1:=r.Group("/api/v1")
v1.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
}))
v1.Get("/users", v1ListUsers)
v1.Get("/posts", v1ListPosts)
// API v2 with additional featuresv2:=r.Group("/api/v2")
v2.Use(middleware.CORS(middleware.AccessControlConfig{
AllowOrigin: "*",
}))
v2.Use(middleware.Timeout(30*time.Second))
v2.Get("/users", v2ListUsers) // New response formatv2.Get("/posts", v2ListPosts) // Additional fieldsv2.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/v2""github.com/nissy/bon/v2/middleware"
)
funcmain() {
r:=bon.NewRouter()
// Public endpointsr.Get("/", homeHandler)
r.Get("/login", loginPageHandler)
r.Post("/login", loginHandler)
// Protected APIapi:=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 authadmin:=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 standalonewebhooks:=r.Route()
webhooks.Post("/webhook/github", githubWebhook)
webhooks.Post("/webhook/stripe", stripeWebhook)
http.ListenAndServe(":8080", r)
}

Benchmarks

Run benchmarks locally with:

go test -bench=. ./...

API Documentation

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

Performance Tips

  1. Route Registration: Static routes are fastest; route priority is static, then parameters, then wildcards
  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.23.6 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

About

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.

Topics

Resources

Stars

15 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages