https://github.com/fox-toolkit/fox
A high-performance Go HTTP router for building reverse proxies and API gateways, including use cases like ingress controllers
https://github.com/fox-toolkit/fox
fox go golang http-router http-server middleware mux performance router server
Last synced: about 2 months ago
JSON representation
A high-performance Go HTTP router for building reverse proxies and API gateways, including use cases like ingress controllers
- Host: GitHub
- URL: https://github.com/fox-toolkit/fox
- Owner: fox-toolkit
- License: apache-2.0
- Created: 2022-07-15T11:52:31.000Z (almost 4 years ago)
- Default Branch: master
- Last Pushed: 2026-04-21T14:58:32.000Z (2 months ago)
- Last Synced: 2026-04-21T16:42:52.460Z (2 months ago)
- Topics: fox, go, golang, http-router, http-server, middleware, mux, performance, router, server
- Language: Go
- Homepage:
- Size: 8.25 MB
- Stars: 11
- Watchers: 4
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
- Security: SECURITY.md
Awesome Lists containing this project
- fucking-awesome-go - Fox - A high-performance HTTP router for building reverse proxies and API gateways, with first-class support for mutating routes at runtime. (Web Frameworks / Routers)
- awesome-go-with-stars - Fox - performance HTTP router for building reverse proxies and API gateways, with first-class support for mutating routes at runtime. | 2026-04-26 | (Web Frameworks / Routers)
- awesome-go - Fox - A high-performance HTTP router for building reverse proxies and API gateways, with first-class support for mutating routes at runtime. (Web Frameworks / Routers)
README
# Fox

[](https://pkg.go.dev/github.com/fox-toolkit/fox)
[](https://github.com/fox-toolkit/fox/actions?query=workflow%3Atests)
[](https://goreportcard.com/report/github.com/fox-toolkit/fox)
[](https://codecov.io/gh/fox-toolkit/fox)


> [!NOTE]
> This repository has been transferred from `github.com/tigerwill90/fox` to `github.com/fox-toolkit/fox`.
> Existing users should update their imports and `go.mod` accordingly.
Fox is a lightweight and high performance HTTP request router for [Go](https://go.dev/), designed for building reverse proxies,
API gateways, or other applications that require managing routes at runtime based on configuration changes or external events.
It is also well-suited for general use cases such as microservices and REST APIs, though it focuses on routing and does not include
convenience helpers found in full-featured frameworks, such as automatic binding, content negotiation, file uploads, cookies, etc.
Fox supports **mutation on its routing tree while handling requests concurrently**. Internally, it uses a Radix Tree that supports
**lock-free** reads while allowing a concurrent writer, and is optimized for high-concurrency reads and low-concurrency writes.
The router supports complex routing patterns, enforces clear priority rules, and performs strict validation to prevent misconfigurations.
## Disclaimer
The current API is not yet stabilized. Breaking changes may occur before `v1.0.0` and will be noted on the release note.
## Features
**Runtime updates:** Register, update and delete route handler safely at any time without impact on performance.
**Flexible routing:** Fox strikes a balance between routing flexibility, performance and clarity by enforcing clear priority rules, ensuring that
there are no unintended matches and maintaining high performance even for complex routing patterns. Supported features include named parameters,
suffix and infix catch-all, regexp constraints, hostname matching, method and method-less routes, route matchers, and sub-routers.
**Trailing slash handling:** Automatically handle trailing slash inconsistencies by either ignoring them, redirecting to
the canonical path, or enforcing strict matching based on your needs.
**Path correction:** Automatically handle malformed paths with extra slashes or dots by either serving the cleaned path directly or redirecting to the canonical form.
**Automatic OPTIONS replies:** Fox has built-in native support for [OPTIONS requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS).
**Client IP Derivation:** Accurately determine the "real" client IP address using best practices tailored to your network topology.
**Growing middleware ecosystem:** Fox's middleware ecosystem is still limited, but standard `http.Handler` middleware are fully compatible. Contributions are welcome!
---
* [Getting started](#getting-started)
* [Install](#install)
* [Basic example](#basic-example)
* [Named parameters](#named-parameters)
* [Named wildcards](#named-wildcards-catch-all)
* [Route matchers](#route-matchers)
* [Method-less routes](#method-less-routes)
* [Sub-Routers](#sub-routers)
* [Hostname validation & restrictions](#hostname-validation--restrictions)
* [Path encoding](#path-encoding)
* [Priority rules](#priority-rules)
* [Hostname routing](#hostname-routing)
* [Warning about context](#warning-about-context)
* [Concurrency](#concurrency)
* [Managing routes at runtime](#managing-routes-at-runtime)
* [ACID Transaction](#acid-transaction)
* [Managed read-write transaction](#managed-read-write-transaction)
* [Unmanaged read-write transaction](#unmanaged-read-write-transaction)
* [Managed read-only transaction](#managed-read-only-transaction)
* [Middleware](#middleware)
* [Official middlewares](#official-middlewares)
* [Working with http.Handler](#working-with-httphandler)
* [Handling OPTIONS Requests and CORS Automatically](#handling-options-requests-and-cors-automatically)
* [Resolving Client IP](#resolving-client-ip)
* [Benchmark](#benchmark)
* [Road to v1](#road-to-v1)
* [Contributions](#contributions)
* [License](#license)
---
## Getting started
#### Install
With a [correctly configured](https://go.dev/doc/install#testing) Go toolchain:
```shell
go get -u github.com/fox-toolkit/fox
```
This library requires [Go 1.26.0](https://tip.golang.org/doc/go1.26) or above. Per [the Go release policy](https://go.dev/doc/devel/release#policy),
it only supports the two most recent major releases of Go, i.e. 1.26 and 1.25.
#### Basic example
````go
package main
import (
"errors"
"fmt"
"log"
"net/http"
"github.com/fox-toolkit/fox"
)
func HelloServer(c *fox.Context) {
_ = c.String(http.StatusOK, fmt.Sprintf("Hello %s\n", c.Param("name")))
}
func main() {
f := fox.MustRouter(fox.DefaultOptions())
f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/hello/{name}", HelloServer)
if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalln(err)
}
}
````
#### Named parameters
Routes can include named parameters using curly braces `{name}` to match exactly one non-empty route segment. The matching
segments are recorded as [Param](https://pkg.go.dev/github.com/fox-toolkit/fox#Param) and accessible via the
[Context](https://pkg.go.dev/github.com/fox-toolkit/fox#Context). Named parameters are supported anywhere in
the route, but only one parameter is allowed per segment (or hostname label) and must appear at the end of the segment.
````
Pattern /avengers/{name}
/avengers/ironman matches
/avengers/thor matches
/avengers/hulk/angry no matches
/avengers/ no matches
Pattern /users/uuid:{id}
/users/uuid:123 matches
/users/uuid: no matches
Pattern /users/uuid:{id}/config
/users/uuid:123/config matches
/users/uuid:/config no matches
Pattern {sub}.example.com/avengers
first.example.com/avengers matches
example.com/avengers no matches
````
Named parameters can include regular expression using the syntax `{name:regexp}`. Regular expressions cannot
contain capturing groups, but can use non-capturing groups `(?:pattern)` instead. Regexp support is opt-in via
`fox.AllowRegexpParam(true)` option.
````
Pattern /products/{name:[A-Za-z]+}
/products/laptop matches
/products/123 no matches
````
#### Named Wildcards (Catch-all)
Named wildcard start with a plus sign `+` followed by a name `{param}` and match any sequence of characters
including slashes, but cannot match an empty string. The matching segments are also accessible via
[Context](https://pkg.go.dev/github.com/fox-toolkit/fox#Context). Catch-all parameters are supported anywhere in the route,
but only one parameter is allowed per segment (or hostname label) and must appear at the end of the segment.
Consecutive catch-all parameter are not allowed.
````
Pattern /src/+{filepath}
/src/conf.txt matches
/src/dir/config.txt matches
/src/ no matches
Pattern /src/file=+{path}
/src/file=config.txt matches
/src/file=/dir/config.txt matches
/src/file= no matches
Pattern: /assets/+{path}/thumbnail
/assets/images/thumbnail matches
/assets/photos/2021/thumbnail matches
/assets//thumbnail no matches
Pattern +{sub}.example.com/avengers
first.example.com/avengers matches
first.second.example.com/avengers matches
example.com/avengers no matches
````
Optional named wildcard start with an asterisk `*` followed by a name `{param}` and match any sequence of characters
**including empty** strings. Unlike `+{param}`, optional wildcards can only be used as a suffix.
````
Pattern /src/*{filepath}
/src/conf.txt matches
/src/dir/config.txt matches
/src/ matches
Pattern /src/file=*{path}
/src/file=config.txt matches
/src/file=/dir/config.txt matches
/src/file= matches
````
Named wildcards can include a regular expression constraint using the syntax `+{name:regexp}`. Regular expressions cannot
contain capturing groups, but can use non-capturing groups `(?:pattern)` instead. Optional wildcards (`*{param}`) do not
support regular expressions. Regexp support is opt-in via `fox.AllowRegexpParam(true)` option.
````
Pattern /src/+{filepath:[A-Za-z/]+\.json}
/src/dir/config.json matches
/src/dir/config.txt no matches
````
#### Route matchers
Route matchers enable routing decisions based on request properties beyond methods, hostname and path. Multiple routes can share
the same pattern and methods and be differentiated by query parameters, headers, client IP, or custom criteria.
````go
f.MustAdd(fox.MethodGet, "/api/users", V1Handler, fox.WithHeaderMatcher("X-API-Version", "v1"))
f.MustAdd(fox.MethodGet, "/api/users", V2Handler, fox.WithHeaderMatcher("X-API-Version", "v2"))
f.MustAdd(fox.MethodGet, "/api/users", V1Handler) // Fallback route
````
Built-in matchers include `fox.WithQueryMatcher`, `fox.WithQueryRegexpMatcher`, `fox.WithHeaderMatcher`, `fox.WithHeaderRegexpMatcher`,
`WithSchemeMatcher` and `fox.WithClientIPMatcher`. Multiple matchers on a route use AND logic. Routes without matchers serve as fallbacks.
For custom matching logic, implement the `fox.Matcher` interface and use `fox.WithMatcher`. See [Priority rules](#priority-rules) for matcher
evaluation order.
#### Method-less routes
Routes can be registered without specifying an HTTP method to match any method. The constant `fox.MethodAny` is
a convenience placeholder equivalent to an empty method set (nil or empty slice).
````go
// Handle any method on /health
f.MustAdd(fox.MethodAny, "/health", HealthHandler)
// Forward all requests to a backend service
f.MustAdd(fox.MethodAny, "/api/*{any}", ProxyHandler)
````
Routes registered with a specific HTTP method always take precedence over method-less routes. This allows defining method-specific
behavior while falling back to a generic handler for other methods.
````go
// Specific handler for GET requests
f.MustAdd(fox.MethodGet, "/resource", GetHandler)
// All other methods handled here
f.MustAdd(fox.MethodAny, "/resource", FallbackHandler)
````
#### Sub-Routers
Fox provides a composable routing API where routers can be mounted as regular routes, each with its own middleware and configuration.
```go
api := fox.MustRouter(fox.WithMiddleware(AuthMiddleware()))
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/", HelloHandler)
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users", ListUser)
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users/{id}", GetUser)
api.MustAdd(fox.MethodPost, "/users", CreateUser)
f := fox.MustRouter(fox.DefaultOptions())
f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/*{filepath}", fox.WrapH(http.FileServer(http.Dir("./public/"))))
f.MustAdd(fox.MethodAny, "/api*{mount}", fox.Sub(api))
```
Requests matching the prefix are delegated to the mounted router with the remaining path.
Use cases include:
- Applying middleware, matchers or other configuration to a route prefix
- Managing entire route subtree at runtime (e.g. insert, update, or delete via the parent router)
- Organizing routes into groups with shared configuration
#### Hostname validation & restrictions
Hostnames are validated to conform to the [LDH (letters, digits, hyphens) rule](https://datatracker.ietf.org/doc/html/rfc3696.html#section-2)
(lowercase only) and SRV-like "underscore labels". Wildcard segments within hostnames, such as `{sub}.example.com/`, are exempt from LDH validation
since they act as placeholders rather than actual domain labels. As such, they do not count toward the hard limit of 63 characters per label,
nor the 253-character limit for the full hostname. Internationalized domain names (IDNs) should be specified using an ASCII
(Punycode) representation.
#### Path encoding
Fox matches requests against the canonical encoded path, equivalent to `url.URL.EscapedPath()` with percent-encoded hex
sequences normalized to uppercase (e.g. `%2f` becomes `%2F`). Encoded and decoded forms are not interchangeable so a request
for `/foo%2Fbar` will not match a pattern registered as `/foo/bar`. Patterns containing literal characters that require
encoding must be registered in their encoded form (e.g. `/foo%20bar`, not `/foo bar`).
#### Priority rules
The router is designed to balance routing flexibility, performance, and predictability. Internally, it uses a radix tree to
store routes efficiently. When a request arrives, Fox evaluates routes in the following order:
1. **Hostname matching**
- Routes with hostnames are evaluated before path-only routes
2. **Pattern matching** (longest match, most specific first)
- Static segments
- Named parameters with regex constraints
- Named parameters without constraints
- Catch-all parameters with regex constraints
- Catch-all parameters without constraints
- Infix catch-all are evaluated before suffix catch-all (e.g., `/bucket/+{path}/meta` before `/bucket/+{path}`)
- At the same level, multiple regex-constrained parameters are evaluated in registration order
3. **Method matching**
- Routes with specific methods are evaluated before method-less routes
4. **Matcher evaluation** (for routes sharing the same pattern and overlapping methods)
- Routes with matchers are evaluated before routes without
- Among routes with matchers, higher priority is evaluated first (configurable via `fox.WithMatcherPriority`, or defaults to the number of matchers)
- Routes with equal matchers priority may be evaluated in any order
If a match candidate fails to complete the full route, including matchers, Fox returns to the last decision point and tries the next available
alternative following the same priority order.
##### Hostname routing
The router can transition instantly and transparently from path-only mode to hostname-prioritized mode without any
additional configuration or action. If any route with a hostname is registered, the router automatically switches to
prioritize hostname matching. Conversely, if no hostname-specific routes are registered, the router reverts to
path-priority mode.
- If the router has no routes registered with hostnames, the router will perform a path-based lookup only.
- If the router includes at least one route with a hostname, the router will prioritize lookup based
on the request host and path. If no match is found, the router will then fall back to a path-only lookup.
Hostname matching is **case-insensitive**, so requests to `Example.COM`, `example.com`, and `EXAMPLE.COM` will all match a route registered for `example.com`.
#### Warning about context
The `fox.Context` instance is freed once the request handler function returns to optimize resource allocation.
If you need to retain `fox.Context` beyond the scope of the handler, use the `fox.Context.Clone` methods.
````go
func Hello(c *fox.Context) {
cc := c.Clone()
go func() {
time.Sleep(2 * time.Second)
log.Println(cc.Param("name")) // Safe
}()
_ = c.String(http.StatusOK, "Hello %s\n", c.Param("name"))
}
````
## Concurrency
Fox implements an **immutable radix tree** with copy-on-write semantics, which support lock-free reads while allowing
a single concurrent writer. Mutations follow a three-phase pattern: first, descend recursively through the tree to
locate the insertion point; then as the call stack unwinds, copy each node along the modified path back to the root and finally,
update the root in a **single atomic operation**. The result is a shallow copy of the tree, where unmodified branches
are shared between the old and new tree. Multiple mutations can be applied in a single transaction, where each cloned node is cached
to avoid copying it more than once.
### Other key points
- Routing requests is lock-free (reading thread never block, even while writes are ongoing)
- The router always sees a consistent version of the tree while routing request
- Reading threads do not block writing threads (adding, updating or removing a handler can be done concurrently)
- Writing threads block each other but never block reading threads
As such threads that route requests should never encounter latency due to ongoing writes or other concurrent readers.
### Managing routes at runtime
#### Routing mutation
In this example, the handler for `routes/{action}` allows to dynamically register, update and delete handler for the
given route and method. Thanks to Fox's design, those actions are perfectly safe and may be executed concurrently.
````go
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
"github.com/fox-toolkit/fox"
)
type Data struct {
Pattern string `json:"pattern"`
Methods []string `json:"methods"`
Text string `json:"text"`
}
func Action(c *fox.Context) {
data := new(Data)
if err := json.NewDecoder(c.Request().Body).Decode(data); err != nil {
http.Error(c.Writer(), err.Error(), http.StatusBadRequest)
return
}
var err error
action := c.Param("action")
switch action {
case "add":
_, err = c.Router().Add(data.Methods, data.Pattern, func(c *fox.Context) {
_ = c.String(http.StatusOK, data.Text)
})
case "update":
_, err = c.Router().Update(data.Methods, data.Pattern, func(c *fox.Context) {
_ = c.String(http.StatusOK, data.Text)
})
case "delete":
_, err = c.Router().Delete(data.Methods, data.Pattern)
default:
http.Error(c.Writer(), fmt.Sprintf("action %q is not allowed", action), http.StatusBadRequest)
return
}
if err != nil {
http.Error(c.Writer(), err.Error(), http.StatusConflict)
return
}
_ = c.String(http.StatusOK, fmt.Sprintf("%s route [%s] %s: success\n", action, strings.Join(data.Methods, ","), data.Pattern))
}
func main() {
f := fox.MustRouter(fox.DefaultOptions())
f.MustAdd(fox.MethodPost, "/routes/{action}", Action)
if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalln(err)
}
}
````
#### ACID Transaction
Fox supports read-write and read-only transactions (with Atomicity, Consistency, and Isolation; Durability is not supported
as transactions are in memory). Thread that route requests always see a consistent version of the routing tree and are
fully isolated from an ongoing transaction until committed. Read-only transactions capture a point-in-time snapshot of
the tree, ensuring they do not observe any ongoing or committed changes made after their creation.
#### Managed read-write transaction
````go
// Updates executes a function within the context of a read-write managed transaction. If no error is returned
// from the function then the transaction is committed. If an error is returned then the entire transaction is
// aborted.
if err := f.Updates(func(txn *fox.Txn) error {
if _, err := txn.Add(fox.MethodGet, "example.com/hello/{name}", Handler); err != nil {
return err
}
// Iter returns a collection of range iterators for traversing registered routes.
it := txn.Iter()
// When Iter() is called on a write transaction, it creates a point-in-time snapshot of the transaction state.
// It means that writing on the current transaction while iterating is allowed, but the mutation will not be
// observed in the result returned by PatternPrefix (or any other iterator).
for route := range it.PatternPrefix("tmp.example.com/") {
if _, err := txn.Delete(slices.Collect(route.Methods()), route.Pattern()); err != nil {
return err
}
}
return nil
}); err != nil {
log.Printf("transaction aborted: %s", err)
}
````
#### Managed read-only transaction
````go
_ = f.View(func(txn *fox.Txn) error {
if txn.Has(fox.MethodGet, "/foo") {
if txn.Has(fox.MethodGet, "/bar") {
// do something
}
}
return nil
})
````
#### Unmanaged read-write transaction
````go
// Txn create an unmanaged read-write or read-only transaction.
txn := f.Txn(true)
defer txn.Abort()
if _, err := txn.Add(fox.MethodGet, "example.com/hello/{name}", Handler); err != nil {
log.Printf("error inserting route: %s", err)
return
}
// Iter returns a collection of range iterators for traversing registered routes.
it := txn.Iter()
// When Iter() is called on a write transaction, it creates a point-in-time snapshot of the transaction state.
// It means that writing on the current transaction while iterating is allowed, but the mutation will not be
// observed in the result returned by PatternPrefix (or any other iterator).
for route := range it.PatternPrefix("tmp.example.com/") {
if _, err := txn.Delete(slices.Collect(route.Methods()), route.Pattern()); err != nil {
log.Printf("error deleting route: %s", err)
return
}
}
// Finalize the transaction
txn.Commit()
````
## Middleware
Middlewares can be registered globally using the `fox.WithMiddleware` option. The example below demonstrates how
to create and apply automatically a simple logging middleware to all routes (including 404, 405, etc...).
````go
package main
import (
"log"
"net/http"
"time"
"github.com/fox-toolkit/fox"
)
func Logger(next fox.HandlerFunc) fox.HandlerFunc {
return func(c *fox.Context) {
start := time.Now()
next(c)
log.Printf("route: %s, latency: %s, status: %d, size: %d",
c.Pattern(),
time.Since(start),
c.Writer().Status(),
c.Writer().Size(),
)
}
}
func main() {
f := fox.MustRouter(fox.WithMiddleware(Logger))
f.MustAdd(fox.MethodGet, "/", func(c *fox.Context) {
_ = c.String(http.StatusOK, "Hello World")
})
log.Fatalln(http.ListenAndServe(":8080", f))
}
````
Additionally, `fox.WithMiddlewareFor` option provide a more fine-grained control over where a middleware is applied, such as
only for 404 or 405 handlers. Possible scopes include `fox.RouteHandler` (regular routes), `fox.NoRouteHandler`, `fox.NoMethodHandler`,
`fox.RedirectSlashHandler`, `fox.RedirectPathHandler`, `fox.OptionsHandler` and any combination of these.
````go
f := fox.MustRouter(
fox.WithMiddlewareFor(fox.RouteHandler, Logger),
fox.WithMiddlewareFor(fox.NoRouteHandler|fox.NoMethodHandler, SpecialLogger),
)
````
Finally, it's also possible to attaches middleware on a per-route basis. Note that route-specific middleware must be explicitly reapplied
when updating a route. If not, any middleware will be removed, and the route will fall back to using only global middleware (if any).
````go
f := fox.MustRouter(
fox.WithMiddleware(fox.Logger(slog.NewTextHandler(os.Stdout, nil))),
)
f.MustAdd(fox.MethodGet, "/", SomeHandler, fox.WithMiddleware(foxtimeout.Middleware(2*time.Second)))
f.MustAdd(fox.MethodGet, "/foo", SomeOtherHandler)
````
### Official middlewares
* [fox-toolkit/oteltracing](https://github.com/fox-toolkit/oteltracing): Distributed tracing with [OpenTelemetry](https://opentelemetry.io/)
* [fox-toolkit/timeout](https://github.com/fox-toolkit/timeout): Better `http.TimeoutHandler` middleware.
* [fox-toolkit/waf](https://github.com/fox-toolkit/waf): Coraza WAF middleware (experimental).
## Working with http.Handler
Fox itself implements the `http.Handler` interface which make easy to chain any compatible middleware before the router. Moreover, the router
provides convenient `fox.WrapF`, `fox.WrapH` and `fox.WrapM` adapter to be use with `http.Handler`.
The route parameters can be accessed by the wrapped handler through the request `context.Context` when the adapters are used.
Wrapping an `http.Handler`
````go
articles := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
params := fox.ParamsFromContext(r.Context())
// Article id: 80
// Matched route: /articles/{id}
_, _ = fmt.Fprintf(w, "Article id: %s\nMatched route: %s\n", params.Get("id"), r.Pattern)
})
f := fox.MustRouter()
f.MustAdd(fox.MethodGet, "/articles/{id}", fox.WrapH(articles))
````
Wrapping any standard `http.Handler` middleware
````go
corsMw, _ := cors.NewMiddleware(cors.Config{
Origins: []string{"https://example.com"},
Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut},
RequestHeaders: []string{"Authorization"},
})
f := fox.MustRouter(
fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
)
````
## Handling OPTIONS Requests and CORS Automatically
The `WithAutoOptions` setting or the `WithOptionsHandler` registration enable automatic responses to [OPTIONS requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS).
This feature is particularly useful for handling Cross-Origin Resource Sharing (CORS) preflight requests.
When automatic OPTIONS responses is enabled, Fox distinguishes between regular OPTIONS requests and CORS preflight requests:
- **Regular OPTIONS requests:** The router responds with the `Allow` header populated with all HTTP methods registered for the matched resource. If no route matches, the `NoRoute` handler is called.
- **CORS preflight requests:** The router responds to every preflight request by calling the OPTIONS handler, regardless of whether the resource exists.
To customize how OPTIONS requests are handled (e.g. adding CORS headers), you may register a middleware for the `fox.OptionsHandler` scope
or provide a custom handler via `WithOptionsHandler`. Note that registered routes with the OPTIONS method always take precedence over automatic replies.
````go
package main
import (
"errors"
"log"
"net/http"
"github.com/jub0bs/cors"
"github.com/fox-toolkit/fox"
)
func main() {
corsMw, err := cors.NewMiddleware(cors.Config{
Origins: []string{"https://example.com"},
Methods: []string{http.MethodGet, http.MethodPost},
RequestHeaders: []string{"Authorization"},
})
if err != nil {
log.Fatal(err)
}
corsMw.SetDebug(true) // turn debug mode on (optional)
f := fox.MustRouter(
fox.WithAutoOptions(true), // let Fox automatically handle OPTIONS requests
fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
)
f.MustAdd(fox.MethodGet, "/api/users", ListUsers)
f.MustAdd(fox.MethodPost, "/api/users", CreateUsers)
if err := http.ListenAndServe(":8080", f); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
````
Alternatively, you can use a sub-router to apply CORS only to a specific section of your API.
````go
package main
import (
"log"
"net/http"
"github.com/jub0bs/cors"
"github.com/fox-toolkit/fox"
)
func main() {
corsMw, err := cors.NewMiddleware(cors.Config{
Origins: []string{"https://example.com"},
Methods: []string{http.MethodHead, http.MethodGet, http.MethodPost},
RequestHeaders: []string{"Authorization"},
})
if err != nil {
log.Fatal(err)
}
corsMw.SetDebug(true) // turn debug mode on (optional)
f := fox.MustRouter()
f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/*{filepath}", fox.WrapH(http.FileServer(http.Dir("./public/"))))
api := fox.MustRouter(
fox.WithAutoOptions(true), // let Fox automatically handle OPTIONS requests
fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
)
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users", ListUsers)
api.MustAdd(fox.MethodPost, "/users", CreateUser)
f.MustAdd(fox.MethodAny, "/api*{any}", fox.Sub(api)) // note: Method-less route
}
````
The CORS protocol is complex and security-sensitive. We do **NOT** recommend implementing CORS handling manually. Instead,
consider using [jub0bs/cors](https://github.com/jub0bs/cors), which performs extensive validation before allowing middleware creation, helping you avoid common pitfalls.
## Resolving Client IP
The `WithClientIPResolver` option allows you to set up strategies to resolve the client IP address based on your
use case and network topology. Accurately determining the client IP is hard, particularly in environments with proxies or
load balancers. For example, the leftmost IP in the `X-Forwarded-For` header is commonly used and is often regarded as the
"closest to the client" and "most real," but it can be easily spoofed. Therefore, you should absolutely avoid using it
for any security-related purposes, such as request throttling.
The resolver used must be chosen and tuned for your network configuration. This should result in a resolver never returning
an error and if it does, it should be treated as an application issue or a misconfiguration, rather than defaulting to an
untrustworthy IP.
The sub-package `github.com/fox-toolkit/fox/clientip` provides a set of best practices resolvers that should cover most use cases.
````go
package main
import (
"fmt"
"github.com/fox-toolkit/fox"
"github.com/fox-toolkit/fox/clientip"
)
func main() {
resolver, err := clientip.NewRightmostNonPrivate(clientip.XForwardedForKey)
if err != nil {
panic(err)
}
f := fox.MustRouter(
fox.DefaultOptions(),
fox.WithClientIPResolver(
resolver,
),
)
f.MustAdd(fox.MethodGet, "/foo/bar", func(c *fox.Context) {
ipAddr, err := c.ClientIP()
if err != nil {
// If the current resolver is not able to derive the client IP, an error
// will be returned rather than falling back on an untrustworthy IP. It
// should be treated as an application issue or a misconfiguration.
panic(err)
}
fmt.Println(ipAddr.String())
})
}
````
It is also possible to create a chain with multiple resolvers that attempt to derive the client IP, stopping when the first one succeeds.
````go
resolver, _ := clientip.NewLeftmostNonPrivate(clientip.ForwardedKey, 10)
f := fox.MustRouter(
fox.DefaultOptions(),
fox.WithClientIPResolver(
// A common use for this is if a server is both directly connected to the
// internet and expecting a header to check.
clientip.NewChain(
resolver,
clientip.NewRemoteAddr(),
),
),
)
````
Note that there is no "sane" default strategy, so calling `Context.ClientIP` without a resolver configured will return
an `ErrNoClientIPResolver`.
See this [blog post](https://adam-p.ca/blog/2022/03/x-forwarded-for/) for general guidance on choosing a strategy that fit your needs.
## Benchmark
The primary goal of Fox is to be a lightweight, high performance router which allow routes modification at runtime.
The following benchmarks attempt to compare Fox to various popular alternatives, including both fully-featured web frameworks
and lightweight request routers. These benchmarks are based on the [julienschmidt/go-http-routing-benchmark](https://github.com/julienschmidt/go-http-routing-benchmark)
repository.
Please note that these benchmarks should not be taken too seriously, as the comparison may not be entirely fair due to
the differences in feature sets offered by each framework. Performance should be evaluated in the context of your specific
use case and requirements. While Fox aims to excel in performance, it's important to consider the trade-offs and
functionality provided by different web frameworks and routers when making your selection.
### Config
```
GOOS: Darwin
GOARCH: arm64
GO: 1.26
CPU: Apple M4 Max
```
### Static Routes
It is just a collection of random static paths inspired by the structure of the Go directory. It might not be a realistic URL-structure.
**GOMAXPROCS: 1**
```
BenchmarkHttpRouter_StaticAll 624214 3800 ns/op 0 B/op 0 allocs/op
BenchmarkHttpTreeMux_StaticAll 475593 5077 ns/op 0 B/op 0 allocs/op
BenchmarkGin_StaticAll 437480 5461 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_StaticAll 298819 7452 ns/op 0 B/op 0 allocs/op
BenchmarkFox_StaticAll 292527 8102 ns/op 0 B/op 0 allocs/op
BenchmarkStdMux_StaticAll 186615 12339 ns/op 0 B/op 0 allocs/op
BenchmarkChi_StaticAll 63331 37896 ns/op 57776 B/op 314 allocs/op
BenchmarkBeego_StaticAll 42171 57150 ns/op 55264 B/op 471 allocs/op
BenchmarkGorillaMux_StaticAll 8614 293244 ns/op 133137 B/op 1099 allocs/op
BenchmarkPat_StaticAll 5626 414071 ns/op 602832 B/op 12559 allocs/op
BenchmarkMartini_StaticAll 4375 562980 ns/op 129210 B/op 2031 allocs/op
BenchmarkTraffic_StaticAll 3708 646128 ns/op 749842 B/op 14444 allocs/op
```
In this benchmark, Fox performs as well as `Gin` and `Echo` which are both Radix Tree based routers. An interesting fact is
that [HttpTreeMux](https://github.com/dimfeld/httptreemux) also support [adding route while serving request concurrently](https://github.com/dimfeld/httptreemux#concurrency).
However, it takes a slightly different approach, by using an optional `RWMutex` that may not scale as well as Fox under heavy load. The next
test compare `HttpTreeMux` with and without the `*SafeAddRouteFlag` (concurrent reads and writes) and `Fox` in parallel benchmark.
**GOMAXPROCS: 16**
```
BenchmarkFox_StaticAllParallel-16 3309738 739.3 ns/op 0 B/op 0 allocs/op
BenchmarkHttpTreeMux_StaticAllParallel-16 100099 23991 ns/op 0 B/op 0 allocs/op
```
As you can see, this benchmark highlight the cost of using higher synchronisation primitive like `RWMutex` to be able to register new route while handling requests.
### Micro Benchmarks
The following benchmarks measure the cost of some very basic operations.
In the first benchmark, only a single route, containing a parameter, is loaded into the routers. Then a request for a URL
matching this pattern is made and the router has to call the respective registered handler function. End.
**GOMAXPROCS: 1**
```
BenchmarkEcho_Param 100000000 21.37 ns/op 0 B/op 0 allocs/op
BenchmarkGin_Param 100000000 23.43 ns/op 0 B/op 0 allocs/op
BenchmarkFox_Param 89916406 26.83 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_Param 79691304 30.77 ns/op 32 B/op 1 allocs/op
BenchmarkHttpTreeMux_Param 15789637 152.6 ns/op 352 B/op 3 allocs/op
BenchmarkBeego_Param 7728720 314.2 ns/op 352 B/op 3 allocs/op
BenchmarkPat_Param 7304704 316.0 ns/op 472 B/op 8 allocs/op
BenchmarkChi_Param 7349187 327.5 ns/op 704 B/op 4 allocs/op
BenchmarkGorillaMux_Param 4107637 583.5 ns/op 1152 B/op 8 allocs/op
BenchmarkTraffic_Param 2434245 944.6 ns/op 1808 B/op 19 allocs/op
BenchmarkMartini_Param 2001694 1215 ns/op 1096 B/op 12 allocs/op
```
Same as before, but now with multiple parameters, all in the same single route. The intention is to see how the routers scale with the number of parameters.
**GOMAXPROCS: 1**
```
BenchmarkGin_Param5 60474345 41.07 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_Param5 54142920 43.99 ns/op 0 B/op 0 allocs/op
BenchmarkFox_Param5 38424334 62.01 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_Param5 29863018 80.66 ns/op 160 B/op 1 allocs/op
BenchmarkHttpTreeMux_Param5 7086744 338.2 ns/op 576 B/op 6 allocs/op
BenchmarkBeego_Param5 5765439 412.4 ns/op 352 B/op 3 allocs/op
BenchmarkChi_Param5 5436216 444.1 ns/op 704 B/op 4 allocs/op
BenchmarkPat_Param5 3452257 707.6 ns/op 776 B/op 23 allocs/op
BenchmarkGorillaMux_Param5 2546830 934.7 ns/op 1216 B/op 8 allocs/op
BenchmarkMartini_Param5 1664374 1436 ns/op 1256 B/op 13 allocs/op
BenchmarkTraffic_Param5 1617499 1445 ns/op 2176 B/op 26 allocs/op
BenchmarkEcho_Param20 20749047 116.1 ns/op 0 B/op 0 allocs/op
BenchmarkGin_Param20 20870449 117.1 ns/op 0 B/op 0 allocs/op
BenchmarkFox_Param20 10336230 234.9 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_Param20 8866296 269.5 ns/op 704 B/op 1 allocs/op
BenchmarkBeego_Param20 2399250 995.7 ns/op 352 B/op 3 allocs/op
BenchmarkChi_Param20 1392295 1735 ns/op 2504 B/op 9 allocs/op
BenchmarkHttpTreeMux_Param20 1300804 1843 ns/op 3144 B/op 13 allocs/op
BenchmarkGorillaMux_Param20 1000000 2065 ns/op 3272 B/op 13 allocs/op
BenchmarkMartini_Param20 868604 2716 ns/op 3568 B/op 18 allocs/op
BenchmarkPat_Param20 695614 3433 ns/op 3992 B/op 75 allocs/op
BenchmarkTraffic_Param20 453583 5186 ns/op 7664 B/op 52 allocs/op
```
Now let's see how expensive it is to access a parameter. The handler function reads the value (by the name of the parameter, e.g. with a map
lookup; depends on the router) and writes it to `/dev/null`
**GOMAXPROCS: 1**
```
BenchmarkGin_ParamWrite 81540667 27.21 ns/op 0 B/op 0 allocs/op
BenchmarkFox_ParamWrite 69228854 34.00 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_ParamWrite 69800920 34.05 ns/op 32 B/op 1 allocs/op
BenchmarkEcho_ParamWrite 46682539 49.16 ns/op 8 B/op 1 allocs/op
BenchmarkHttpTreeMux_ParamWrite 15320574 160.0 ns/op 352 B/op 3 allocs/op
BenchmarkChi_ParamWrite 7458879 323.0 ns/op 704 B/op 4 allocs/op
BenchmarkBeego_ParamWrite 7340652 327.7 ns/op 360 B/op 4 allocs/op
BenchmarkPat_ParamWrite 4767104 501.8 ns/op 896 B/op 12 allocs/op
BenchmarkGorillaMux_ParamWrite 4066573 592.5 ns/op 1152 B/op 8 allocs/op
BenchmarkTraffic_ParamWrite 2079380 1148 ns/op 2232 B/op 23 allocs/op
BenchmarkMartini_ParamWrite 1760973 1355 ns/op 1144 B/op 15 allocs/op
```
In those micro benchmarks, we can see that `Fox` scale really well, even with long wildcard routes. Like `Gin`, this router reuse the
data structure (e.g. `fox.Context` slice) containing the matching parameters in order to remove completely heap allocation.
### Github
Finally, this benchmark executes a request for each GitHub API route (203 routes).
**GOMAXPROCS: 1**
```
BenchmarkGin_GithubAll 286442 8405 ns/op 0 B/op 0 allocs/op
BenchmarkEcho_GithubAll 207840 11112 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_GithubAll 179312 13219 ns/op 14240 B/op 171 allocs/op
BenchmarkFox_GithubAll 170670 14033 ns/op 0 B/op 0 allocs/op
BenchmarkHttpTreeMux_GithubAll 58237 41668 ns/op 67648 B/op 691 allocs/op
BenchmarkChi_GithubAll 30548 78991 ns/op 130817 B/op 740 allocs/op
BenchmarkBeego_GithubAll 30604 79720 ns/op 73121 B/op 629 allocs/op
BenchmarkTraffic_GithubAll 2248 1071896 ns/op 837296 B/op 14315 allocs/op
BenchmarkPat_GithubAll 2174 1104015 ns/op 1834945 B/op 28773 allocs/op
BenchmarkGorillaMux_GithubAll 1900 1272217 ns/op 230339 B/op 1620 allocs/op
BenchmarkMartini_GithubAll 1728 1375748 ns/op 236943 B/op 2805 allocs/op
```
## Road to v1
- [x] [Update route syntax](https://github.com/fox-toolkit/fox/pull/10#issue-1643728309) @v0.6.0
- [x] [Route overlapping](https://github.com/fox-toolkit/fox/pull/9#issue-1642887919) @v0.7.0
- [x] [Route overlapping (catch-all and params)](https://github.com/fox-toolkit/fox/pull/24#issue-1784686061) @v0.10.0
- [x] [Ignore trailing slash](https://github.com/fox-toolkit/fox/pull/32), [Builtin Logger Middleware](https://github.com/fox-toolkit/fox/pull/33), [Client IP Derivation](https://github.com/fox-toolkit/fox/pull/33) @v0.14.0
- [x] [Support infix wildcard](https://github.com/fox-toolkit/fox/pull/46), [Support hostname routing](https://github.com/fox-toolkit/fox/pull/48), [Support ACID transaction](https://github.com/fox-toolkit/fox/pull/49) @v0.18.0
- [x] [Support regexp params](https://github.com/fox-toolkit/fox/pull/68) @v0.25.0
- [x] [Support route matchers](https://github.com/fox-toolkit/fox/pull/69), [Support SubRouter](https://github.com/fox-toolkit/fox/pull/70), [Method-less tree](https://github.com/fox-toolkit/fox/pull/71) @v0.26.0
- [x] Programmatic error handling
- [ ] Improving performance and stabilizing API
## Contributions
This project aims to provide a lightweight, high-performance router that is easy to use and hard to misuse, designed for building API gateways and reverse proxies.
Features are chosen carefully with an emphasis on composability, and each addition is evaluated against this core mission. The router exposes a relatively low-level API,
allowing it to serve as a building block for implementing your own "batteries included" frameworks. Feature requests and PRs along these lines are welcome.
## License
Fox is licensed under the **Apache License 2.0**. See [`LICENSE.txt`](./LICENSE.txt) for details.
The [**Fox logo**](https://github.com/fox-toolkit/fox/blob/static/fox_logo.png) is licensed separately under [**CC BY-NC-ND 4.0**](https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1).
See [`LICENSE-fox-logo.txt`](https://github.com/fox-toolkit/fox/blob/static/LICENSE-fox-logo.txt) for details.
## Acknowledgements
- [hashicorp/go-immutable-radix](https://github.com/hashicorp/go-immutable-radix): Fox Tree design is inspired by Hashicorp's Immutable Radix Tree.
- [realclientip/realclientip-go](https://github.com/realclientip/realclientip-go): Fox uses a derivative version of Adam Pritchard's `realclientip-go` library.
See his insightful [blog post](https://adam-p.ca/blog/2022/03/x-forwarded-for/) on the topic for more details.
- The router API is influenced by popular routers such as [Gin](https://github.com/gin-gonic/gin) and [Echo](https://github.com/labstack/echo).