fox

package module
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2024 License: Apache-2.0 Imports: 25 Imported by: 6

README

Go Reference tests Go Report Card codecov GitHub release (latest SemVer) GitHub go.mod Go version

Fox

Fox is a zero allocation, lightweight, high performance HTTP request router for Go. The main difference with other routers is that it supports mutation on its routing tree while handling request concurrently. Internally, Fox use a Concurrent Radix Tree that support lock-free reads while allowing concurrent writes. The router tree is optimized for high-concurrency and high performance reads, and low-concurrency write.

Fox supports various use cases, but it is especially designed for applications that require changes at runtime to their routing structure based on user input, configuration changes, or other runtime events.

Disclaimer

The current api is not yet stabilize. Breaking changes may occur before v1.0.0 and will be noted on the release note.

Features

Runtime updates: Register, update and remove route handler safely at any time without impact on performance. Fox never block while serving request!

Wildcard pattern: Route can be registered using wildcard parameters. The matched path segment can then be easily retrieved by name. Due to Fox design, wildcard route are cheap and scale really well.

Detect panic: Comes with a ready-to-use, efficient Recovery middleware that gracefully handles panics.

Get the current route: You can easily retrieve the route of the matched request. This actually makes it easier to integrate observability middleware like open telemetry.

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 pattern.

Redirect trailing slashes: Redirect automatically the client, at no extra cost, if another route matches, with or without a trailing slash.

Ignore trailing slashes: In contrast to redirecting, this option allows the router to handle requests regardless of an extra or missing trailing slash, at no extra cost.

Automatic OPTIONS replies: Fox has built-in native support for OPTIONS requests.

Client IP Derivation: Accurately determine the "real" client IP address using best practices tailored to your network topology.

Of course, you can also register custom NotFound and MethodNotAllowed handlers.

Getting started

Installation
go get -u github.com/tigerwill90/fox
Basic example
package main

import (
	"github.com/tigerwill90/fox"
	"log"
	"net/http"
)

type Greeting struct {
	Say string
}

func (h *Greeting) Greet(c fox.Context) {
	_ = c.String(http.StatusOK, "%s %s\n", h.Say, c.Param("name"))
}

func main() {
	f := fox.New(fox.DefaultOptions())

	err := f.Handle(http.MethodGet, "/", func(c fox.Context) {
		_ = c.String(http.StatusOK, "Welcome\n")
	})
	if err != nil {
		panic(err)
	}

	h := Greeting{Say: "Hello"}
	f.MustHandle(http.MethodGet, "/hello/{name}", h.Greet)

	log.Fatalln(http.ListenAndServe(":8080", f))
}
Error handling

Since new route may be added at any given time, Fox, unlike other router, does not panic when a route is malformed or conflicts with another. Instead, it returns the following error values:

ErrRouteExist    = errors.New("route already registered")
ErrRouteConflict = errors.New("route conflict")
ErrInvalidRoute  = errors.New("invalid route")

Conflict error may be unwrapped to retrieve conflicting route.

if errors.Is(err, fox.ErrRouteConflict) {
    matched := err.(*fox.RouteConflictError).Matched
    for _, route := range matched {
        fmt.Println(route)
    }
}
Named parameters

A route can be defined using placeholder (e.g {name}). The matching segment are recorder into the fox.Params slice accessible via fox.Context. The Param and Get methods are helpers to retrieve the value using the placeholder name.

Pattern /avengers/{name}

/avengers/ironman       match
/avengers/thor          match
/avengers/hulk/angry    no match
/avengers/              no match

Pattern /users/uuid:{id}

/users/uuid:123             match
/users/uuid                 no match
Catch all parameter

Catch-all parameters can be used to match everything at the end of a route. The placeholder start with * followed by a regular named parameter (e.g. *{name}).

Pattern /src/*{filepath}

/src/                       match
/src/conf.txt               match
/src/dir/config.txt         match

Patter /src/file=*{path}

/src/file=                  match
/src/file=config.txt        match
/src/file=/dir/config.txt   match
Priority rules

Routes are prioritized based on specificity, with static segments taking precedence over wildcard segments.

The following rules apply:

  • Static segments are always evaluated first.
  • A named parameter can only overlap with a catch-all parameter or static segments.
  • A catch-all parameter can only overlap with a named parameter or static segments.
  • When a named parameter overlaps with a catch-all parameter, the named parameter is evaluated first.

For instance, GET /users/{id} and GET /users/{name}/profile cannot coexist, as the {id} and {name} segments are overlapping. These limitations help to minimize the number of branches that need to be evaluated in order to find the right match, thereby maintaining high-performance routing.

For example, the followings route are allowed:

GET /*{filepath}
GET /users/{id}
GET /users/{id}/emails
GET /users/{id}/{actions}
POST /users/{name}/emails

Additionally, let's consider an example to illustrate the prioritization:

GET /fs/avengers.txt    #1 => match /fs/avengers.txt
GET /fs/{filename}      #2 => match /fs/ironman.txt
GET /fs/*{filepath}     #3 => match /fs/avengers/ironman.txt
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 or fox.Params beyond the scope of the handler, use the Clone methods.

func Hello(c fox.Context) {
    cc := c.Clone()
    // cp := c.Params().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 a Concurrent Radix Tree that supports lock-free reads while allowing concurrent writes, by calculating the changes which would be made to the tree were it mutable, and assembling those changes into a patch, which is then applied to the tree in a single atomic operation.

For example, here we are inserting the new key toast into to the tree which require an existing node to be split:

When traversing the tree during a patch, reading threads will either see the old version or the new version of the (sub-)tree, but both version are consistent view of the tree.

Other key points
  • Routing requests is lock-free (reading thread never block, even while writes are ongoing)
  • The router always see 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 a runtime
Routing mutation

In this example, the handler for routes/{action} allow to dynamically register, update and remove handler for the given route and method. Thanks to Fox's design, those actions are perfectly safe and may be executed concurrently.

package main

import (
	"encoding/json"
	"fmt"
	"github.com/tigerwill90/fox"
	"log"
	"net/http"
	"strings"
)

func Action(c fox.Context) {
	var data map[string]string
	if err := json.NewDecoder(c.Request().Body).Decode(&data); err != nil {
		http.Error(c.Writer(), err.Error(), http.StatusBadRequest)
		return
	}

	method := strings.ToUpper(data["method"])
	path := data["path"]
	text := data["text"]

	if path == "" || method == "" {
		http.Error(c.Writer(), "missing method or path", http.StatusBadRequest)
		return
	}

	var err error
	action := c.Param("action")
	switch action {
	case "add":
		err = c.Fox().Handle(method, path, func(c fox.Context) {
			_ = c.String(http.StatusOK, text)
		})
	case "update":
		err = c.Fox().Update(method, path, func(c fox.Context) {
			_ = c.String(http.StatusOK, text)
		})
	case "delete":
		err = c.Fox().Remove(method, path)
	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, "%s route [%s] %s: success\n", action, method, path)
}

func main() {
	f := fox.New()
	f.MustHandle(http.MethodPost, "/routes/{action}", Action)
	log.Fatalln(http.ListenAndServe(":8080", f))
}
Tree swapping

Fox also enables you to replace the entire tree in a single atomic operation using the Swap methods. Note that router's options apply automatically on the new tree.

package main

import (
	"fox-by-example/db"
	"github.com/tigerwill90/fox"
	"html/template"
	"log"
	"net/http"
	"strings"
	"time"
)

type HtmlRenderer struct {
	Template template.HTML
}

func (h *HtmlRenderer) Render(c fox.Context) {
	log.Printf("matched handler path: %s", c.Path())
	_ = c.Stream(
		http.StatusOK,
		fox.MIMETextHTMLCharsetUTF8,
		strings.NewReader(string(h.Template)),
	)
}

func main() {
	f := fox.New()
	go Reload(f)
	log.Fatalln(http.ListenAndServe(":8080", f))
}

func Reload(r *fox.Router) {
	for ; true; <-time.Tick(10 * time.Second) {
		routes := db.GetRoutes()
		tree := r.NewTree()
		for _, rte := range routes {
			h := HtmlRenderer{Template: rte.Template}
			if err := tree.Handle(rte.Method, rte.Path, h.Render); err != nil {
				log.Printf("error reloading route: %s\n", err)
				continue
			}
		}
		// Swap the currently in-use routing tree with the new provided.
		r.Swap(tree)
		log.Println("route reloaded")
	}
}
Advanced usage: consistent view updates

In certain situations, it's necessary to maintain a consistent view of the tree while performing updates. The Tree API allow you to take control of the internal sync.Mutex to prevent concurrent writes from other threads. Remember that all write operation should be run serially.

In the following example, the Upsert function needs to perform a lookup on the tree to check if a handler is already registered for the provided method and path. By locking the Tree, this operation ensures atomicity, as it prevents other threads from modifying the tree between the lookup and the write operation. Note that all read operation on the tree remain lock-free.

func Upsert(t *fox.Tree, method, path string, handler fox.HandlerFunc) error {
    t.Lock()
    defer t.Unlock()
    if t.Has(method, path) {
        return t.Update(method, path, handler)
    }
    return t.Handle(method, path, handler)
}
Concurrent safety and proper usage of Tree APIs

When working with the Tree API, it's important to keep some considerations in mind. Each instance has its own sync.Mutex that can be used to serialize writes. However, unlike the router API, the lower-level Tree API does not automatically lock the tree when writing to it. Therefore, it is the user's responsibility to ensure all writes are executed serially.

Moreover, since the router tree may be swapped at any given time, you MUST always copy the pointer locally to avoid inadvertently causing a deadlock by locking/unlocking the wrong Tree.

// Good
t := r.Tree()
t.Lock()
defer t.Unlock()

// Dramatically bad, may cause deadlock
r.Tree().Lock()
defer r.Tree().Unlock()

// Dramatically bad, may cause deadlock
func handle(c fox.Context) {
    c.Fox().Tree().Lock()
    defer c.Fox().Tree().Unlock()
}

Note that fox.Context carries a local copy of the Tree that is being used to serve the handler, thereby eliminating the risk of deadlock when using the Tree within the context.

// Ok
func handle(c fox.Context) {
    c.Tree().Lock()
    defer c.Tree().Unlock()
}

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 and fox.WrapH adapter to be use with http.Handler.

The route parameters can be accessed by the wrapped handler through the context.Context when the adapters fox.WrapF and fox.WrapH are used.

Wrapping an http.Handler

articles := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    params := fox.ParamsFromContext(r.Context())
    _, _ = fmt.Fprintf(w, "Article id: %s\n", params.Get("id"))
})

f := fox.New(fox.DefaultOptions())
f.MustHandle(http.MethodGet, "/articles/{id}", fox.WrapH(httpRateLimiter.RateLimit(articles)))

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...).

package main

import (
	"github.com/tigerwill90/fox"
	"log"
	"net/http"
	"time"
)

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.Path(),
			time.Since(start),
			c.Writer().Status(),
			c.Writer().Size(),
		)
	}
}

func main() {
	f := fox.New(fox.WithMiddleware(Logger))

	f.MustHandle(http.MethodGet, "/", func(c fox.Context) {
		resp, err := http.Get("https://api.coindesk.com/v1/bpi/currentprice.json")
		if err != nil {
			http.Error(c.Writer(), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		defer resp.Body.Close()
		_ = c.Stream(http.StatusOK, fox.MIMEApplicationJSON, resp.Body)
	})

	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.RouteHandlers (regular routes), fox.NoRouteHandler, fox.NoMethodHandler, fox.RedirectHandler, fox.OptionsHandler and any combination of these.

f := fox.New(
    fox.WithMethodNotAllowed(true),
    fox.WithMiddlewareFor(fox.RouteHandlers, fox.Recovery(fox.DefaultHandleRecovery), Logger),
    fox.WithMiddlewareFor(fox.NoRouteHandler|fox.NoMethodHandler, SpecialLogger),
)
Official middlewares

Handling OPTIONS Requests and CORS Automatically

The WithAutoOptions setting or the WithOptionsHandler registration enable automatic responses to OPTIONS requests. This feature can be particularly useful in the context of Cross-Origin Resource Sharing (CORS).

An OPTIONS request is a type of HTTP request that is used to determine the communication options available for a given resource or API endpoint. These requests are commonly used as "preflight" requests in CORS to check if the CORS protocol is understood and to get permission to access data based on origin.

When automatic OPTIONS responses is enabled, the router will automatically respond to preflight OPTIONS requests and set the Allow header with the appropriate value. To customize how OPTIONS requests are handled (e.g. adding CORS headers), you may register a middleware for the fox.OptionsHandler scope or override the default handler.

f := fox.New(
    fox.WithOptionsHandler(func(c fox.Context) {
        if c.Header("Access-Control-Request-Method") != "" {
            // Setting CORS headers.
            c.SetHeader("Access-Control-Allow-Methods", c.Writer().Header().Get("Allow"))
            c.SetHeader("Access-Control-Allow-Origin", "*")
        }

        // Respond with a 204 status code.
        c.Writer().WriteHeader(http.StatusNoContent)
    }),
)

Client IP Derivation

The WithClientIPStrategy 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 strategy used must be chosen and tuned for your network configuration. This should result in the strategy 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/tigerwill90/fox/strategy provides a set of best practices strategies that should cover most use cases.

f := fox.New(
    fox.DefaultOptions(),
    fox.WithClientIPStrategy(
        // We are behind one or many trusted proxies that have all private-space IP addresses.
        strategy.NewRightmostNonPrivate(strategy.XForwardedForKey),
    ),
)

f.MustHandle(http.MethodGet, "/foo/bar", func(c fox.Context) {
    ipAddr, err := c.ClientIP()
        if err != nil {
            // If the current strategy 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 strategies that attempt to derive the client IP, stopping when the first one succeeds.

f = fox.New(
    fox.DefaultOptions(),
    fox.WithClientIPStrategy(
        // A common use for this is if a server is both directly connected to the 
        // internet and expecting a header to check.
        strategy.NewChain(
            strategy.NewLeftmostNonPrivate(strategy.ForwardedKey),
            strategy.NewRemoteAddr(),
        ),
    ),
)

Note that there is no "sane" default strategy, so calling Context.ClientIP without a strategy configured will return an ErrNoClientIPStrategy.

See this blog post 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 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:   Linux
GOARCH: amd64
GO:     1.20
CPU:    Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz
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     161659              7570 ns/op               0 B/op          0 allocs/op
BenchmarkHttpTreeMux_StaticAll    132446              8836 ns/op               0 B/op          0 allocs/op
BenchmarkFox_StaticAll            102577             11348 ns/op               0 B/op          0 allocs/op
BenchmarkStdMux_StaticAll          91304             13382 ns/op               0 B/op          0 allocs/op
BenchmarkGin_StaticAll             78224             15433 ns/op               0 B/op          0 allocs/op
BenchmarkEcho_StaticAll            77923             15739 ns/op               0 B/op          0 allocs/op
BenchmarkBeego_StaticAll           10000            101094 ns/op           55264 B/op        471 allocs/op
BenchmarkGorillaMux_StaticAll       2283            525683 ns/op          113041 B/op       1099 allocs/op
BenchmarkMartini_StaticAll          1330            936928 ns/op          129210 B/op       2031 allocs/op
BenchmarkTraffic_StaticAll          1064           1140959 ns/op          753611 B/op      14601 allocs/op
BenchmarkPat_StaticAll               967           1230424 ns/op          602832 B/op      12559 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 also support adding route while serving request concurrently. 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

Route: all

BenchmarkFox_StaticAll-16                          99322             11369 ns/op               0 B/op          0 allocs/op
BenchmarkFox_StaticAllParallel-16                 831354              1422 ns/op               0 B/op          0 allocs/op
BenchmarkHttpTreeMux_StaticAll-16                 135560              8861 ns/op               0 B/op          0 allocs/op
BenchmarkHttpTreeMux_StaticAllParallel-16*        172714              6916 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

BenchmarkFox_Param              33024534                36.61 ns/op            0 B/op          0 allocs/op
BenchmarkEcho_Param             31472508                38.71 ns/op            0 B/op          0 allocs/op
BenchmarkGin_Param              25826832                52.88 ns/op            0 B/op          0 allocs/op
BenchmarkHttpRouter_Param       21230490                60.83 ns/op           32 B/op          1 allocs/op
BenchmarkHttpTreeMux_Param       3960292                280.4 ns/op          352 B/op          3 allocs/op
BenchmarkBeego_Param             2247776                518.9 ns/op          352 B/op          3 allocs/op
BenchmarkPat_Param               1603902                676.6 ns/op          512 B/op         10 allocs/op
BenchmarkGorillaMux_Param        1000000                 1011 ns/op         1024 B/op          8 allocs/op
BenchmarkTraffic_Param            648986                 1686 ns/op         1848 B/op         21 allocs/op
BenchmarkMartini_Param            485839                 2446 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

BenchmarkFox_Param5             16608495                72.84 ns/op            0 B/op          0 allocs/op
BenchmarkGin_Param5             13098740                92.22 ns/op            0 B/op          0 allocs/op
BenchmarkEcho_Param5            12025460                96.33 ns/op            0 B/op          0 allocs/op
BenchmarkHttpRouter_Param5       8233530                148.1 ns/op          160 B/op          1 allocs/op
BenchmarkHttpTreeMux_Param5      1986019                616.9 ns/op          576 B/op          6 allocs/op
BenchmarkBeego_Param5            1836229                655.3 ns/op          352 B/op          3 allocs/op
BenchmarkGorillaMux_Param5        757936                 1572 ns/op         1088 B/op          8 allocs/op
BenchmarkPat_Param5               645847                 1724 ns/op          800 B/op         24 allocs/op
BenchmarkTraffic_Param5           424431                 2729 ns/op         2200 B/op         27 allocs/op
BenchmarkMartini_Param5           424806                 2772 ns/op         1256 B/op         13 allocs/op


BenchmarkGin_Param20             4636416               244.6 ns/op             0 B/op          0 allocs/op
BenchmarkFox_Param20             4667533               250.7 ns/op             0 B/op          0 allocs/op
BenchmarkEcho_Param20            4352486               277.1 ns/op             0 B/op          0 allocs/op
BenchmarkHttpRouter_Param20      2618958               455.2 ns/op           640 B/op          1 allocs/op
BenchmarkBeego_Param20            847029                1688 ns/op           352 B/op          3 allocs/op
BenchmarkHttpTreeMux_Param20      369500                2972 ns/op          3195 B/op         10 allocs/op
BenchmarkGorillaMux_Param20       318134                3561 ns/op          3195 B/op         10 allocs/op
BenchmarkMartini_Param20          223070                5117 ns/op          3619 B/op         15 allocs/op
BenchmarkPat_Param20              157380                7442 ns/op          4094 B/op         73 allocs/op
BenchmarkTraffic_Param20          119677                9864 ns/op          7847 B/op         47 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

BenchmarkFox_ParamWrite                 16707409                72.53 ns/op            0 B/op          0 allocs/op
BenchmarkHttpRouter_ParamWrite          16478174                73.30 ns/op           32 B/op          1 allocs/op
BenchmarkGin_ParamWrite                 15828385                75.73 ns/op            0 B/op          0 allocs/op
BenchmarkEcho_ParamWrite                13187766                95.18 ns/op            8 B/op          1 allocs/op
BenchmarkHttpTreeMux_ParamWrite          4132832               279.9 ns/op           352 B/op          3 allocs/op
BenchmarkBeego_ParamWrite                2172572               554.3 ns/op           360 B/op          4 allocs/op
BenchmarkPat_ParamWrite                  1200334               996.8 ns/op           936 B/op         14 allocs/op
BenchmarkGorillaMux_ParamWrite           1000000              1005 ns/op            1024 B/op          8 allocs/op
BenchmarkMartini_ParamWrite               454255              2667 ns/op            1168 B/op         16 allocs/op
BenchmarkTraffic_ParamWrite               511766              2021 ns/op            2272 B/op         25 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 execute a request for each GitHub API route (203 routes).

GOMAXPROCS: 1

BenchmarkFox_GithubAll             63984             18555 ns/op               0 B/op          0 allocs/op
BenchmarkEcho_GithubAll            49312             23353 ns/op               0 B/op          0 allocs/op
BenchmarkGin_GithubAll             48422             24926 ns/op               0 B/op          0 allocs/op
BenchmarkHttpRouter_GithubAll      45706             26818 ns/op           14240 B/op        171 allocs/op
BenchmarkHttpTreeMux_GithubAll     14731             80133 ns/op           67648 B/op        691 allocs/op
BenchmarkBeego_GithubAll            7692            137926 ns/op           72929 B/op        625 allocs/op
BenchmarkTraffic_GithubAll           636           1916586 ns/op          845114 B/op      14634 allocs/op
BenchmarkMartini_GithubAll           530           2205947 ns/op          238546 B/op       2813 allocs/op
BenchmarkGorillaMux_GithubAll        529           2246380 ns/op          203844 B/op       1620 allocs/op
BenchmarkPat_GithubAll               424           2899405 ns/op         1843501 B/op      29064 allocs/op

Road to v1

Contributions

This project aims to provide a lightweight, high performance and easy to use http router. It purposely has a limited set of features and exposes a relatively low-level api. The intention behind these choices is that it can serve as a building block for implementing your own "batteries included" frameworks. Feature requests and PRs along these lines are welcome.

Acknowledgements

Documentation

Index

Examples

Constants

View Source
const (
	MIMEApplicationJSON                  = "application/json"
	MIMEApplicationJSONCharsetUTF8       = MIMEApplicationJSON + "; " + charsetUTF8
	MIMEApplicationJavaScript            = "application/javascript"
	MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
	MIMEApplicationXML                   = "application/xml"
	MIMEApplicationXMLCharsetUTF8        = MIMEApplicationXML + "; " + charsetUTF8
	MIMETextXML                          = "text/xml"
	MIMETextXMLCharsetUTF8               = MIMETextXML + "; " + charsetUTF8
	MIMEApplicationForm                  = "application/x-www-form-urlencoded"
	MIMEApplicationProtobuf              = "application/protobuf"
	MIMEApplicationMsgpack               = "application/msgpack"
	MIMETextHTML                         = "text/html"
	MIMETextHTMLCharsetUTF8              = MIMETextHTML + "; " + charsetUTF8
	MIMETextPlain                        = "text/plain"
	MIMETextPlainCharsetUTF8             = MIMETextPlain + "; " + charsetUTF8
	MIMEMultipartForm                    = "multipart/form-data"
	MIMEOctetStream                      = "application/octet-stream"
)

MIME types

View Source
const (
	HeaderAccept         = "Accept"
	HeaderAcceptEncoding = "Accept-Encoding"
	// HeaderAllow is the name of the "Allow" header field used to list the set of methods
	// advertised as supported by the target resource. Returning an Allow header is mandatory
	// for status 405 (method not found) and useful for the OPTIONS method in responses.
	// See RFC 7231: https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1
	HeaderAllow               = "Allow"
	HeaderAuthorization       = "Authorization"
	HeaderProxyAuthorization  = "Proxy-Authorization"
	HeaderContentDisposition  = "Content-Disposition"
	HeaderContentEncoding     = "Content-Encoding"
	HeaderContentLength       = "Content-Length"
	HeaderContentType         = "Content-Type"
	HeaderCookie              = "Cookie"
	HeaderSetCookie           = "Set-Cookie"
	HeaderIfModifiedSince     = "If-Modified-Since"
	HeaderLastModified        = "Last-Modified"
	HeaderLocation            = "Location"
	HeaderRetryAfter          = "Retry-After"
	HeaderUpgrade             = "Upgrade"
	HeaderVary                = "Vary"
	HeaderWWWAuthenticate     = "WWW-Authenticate"
	HeaderXForwardedFor       = "X-Forwarded-For"
	HeaderForwarded           = "Forwarded"
	HeaderXForwardedProto     = "X-Forwarded-Proto"
	HeaderXForwardedProtocol  = "X-Forwarded-Protocol"
	HeaderXForwardedSsl       = "X-Forwarded-Ssl"
	HeaderXRealIP             = "X-Real-Ip"
	HeaderXUrlScheme          = "X-Url-Scheme"
	HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
	HeaderXRequestID          = "X-Request-Id"
	HeaderXCorrelationID      = "X-Correlation-Id"
	HeaderXRequestedWith      = "X-Requested-With"
	HeaderServer              = "Server"
	HeaderOrigin              = "Origin"
	HeaderCacheControl        = "Cache-Control"
	HeaderConnection          = "Connection"
	HeaderETag                = "ETag"

	// Access control
	HeaderAccessControlRequestMethod    = "Access-Control-Request-Method"
	HeaderAccessControlRequestHeaders   = "Access-Control-Request-Headers"
	HeaderAccessControlAllowOrigin      = "Access-Control-Allow-Origin"
	HeaderAccessControlAllowMethods     = "Access-Control-Allow-Methods"
	HeaderAccessControlAllowHeaders     = "Access-Control-Allow-Headers"
	HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials"
	HeaderAccessControlExposeHeaders    = "Access-Control-Expose-Headers"
	HeaderAccessControlMaxAge           = "Access-Control-Max-Age"

	// Security
	HeaderStrictTransportSecurity         = "Strict-Transport-Security"
	HeaderXContentTypeOptions             = "X-Content-Type-Options"
	HeaderXXSSProtection                  = "X-XSS-Protection"
	HeaderXFrameOptions                   = "X-Frame-Options"
	HeaderContentSecurityPolicy           = "Content-Security-Policy"
	HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"
	// nolint:gosec
	HeaderXCSRFToken     = "X-CSRF-Token"
	HeaderReferrerPolicy = "Referrer-Policy"

	// Platform Header for single IP
	HeaderCFConnectionIP       = "CF-Connecting-IP"
	HeaderTrueClientIP         = "True-Client-IP"
	HeaderFastClientIP         = "Fastly-Client-IP"
	HeaderXAzureClientIP       = "X-Azure-ClientIP"
	HeaderXAzureSocketIP       = "X-Azure-SocketIP"
	HeaderXAppengineRemoteAddr = "X-Appengine-Remote-Addr"
	HeaderFlyClientIP          = "Fly-Client-IP"
)

Headers

Variables

View Source
var (
	ErrRouteNotFound           = errors.New("route not found")
	ErrRouteExist              = errors.New("route already registered")
	ErrRouteConflict           = errors.New("route conflict")
	ErrInvalidRoute            = errors.New("invalid route")
	ErrDiscardedResponseWriter = errors.New("discarded response writer")
	ErrInvalidRedirectCode     = errors.New("invalid redirect code")
	ErrNoClientIPStrategy      = errors.New("no client ip strategy")
)
View Source
var SkipMethod = errors.New("skip method")

SkipMethod is used as a return value from WalkFunc to indicate that the method named in the call is to be skipped.

Functions

func CleanPath

func CleanPath(p string) string

CleanPath is the URL version of path.Clean, it returns a canonical URL path for p, eliminating . and .. elements.

The following rules are applied iteratively until no further processing can be done:

  1. Replace multiple slashes with a single slash.
  2. Eliminate each . path name element (the current directory).
  3. Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it.
  4. Eliminate .. elements that begin a rooted path: that is, replace "/.." by "/" at the beginning of a path.

If the result of this process is an empty string, "/" is returned

func DefaultHandleRecovery added in v0.7.2

func DefaultHandleRecovery(c Context, _ any)

DefaultHandleRecovery is a default implementation of the RecoveryFunc. It responds with a status code 500 and writes a generic error message.

func NewTestContext added in v0.7.0

func NewTestContext(w http.ResponseWriter, r *http.Request) (*Router, Context)

NewTestContext returns a new Router and its associated Context, designed only for testing purpose.

func Walk added in v0.5.0

func Walk(tree *Tree, fn WalkFunc) error

Walk allow to walk over all registered route in lexicographical order. If the function return the special value SkipMethod, Walk skips the current method. This function is safe for concurrent use by multiple goroutine and while mutation are ongoing. This API is EXPERIMENTAL and is likely to change in future release.

Types

type ClientIPStrategy added in v0.14.0

type ClientIPStrategy interface {
	// ClientIP returns the "real" client IP according to the implemented strategy. It returns an error if no valid IP
	// address can be derived using the strategy. This is typically considered a misconfiguration error, unless the strategy
	// involves obtaining an untrustworthy or optional value.
	ClientIP(c Context) (*net.IPAddr, error)
}

ClientIPStrategy define a strategy for obtaining the "real" client IP from HTTP requests. The strategy used must be chosen and tuned for your network configuration. This should result in the strategy never returning an error i.e., never failing to find a candidate for the "real" IP. Consequently, getting an error result should be treated as an application error, perhaps even worthy of panicking. Builtin best practices strategies can be found in the github.com/tigerwill90/fox/strategy package. See https://adam-p.ca/blog/2022/03/x-forwarded-for/ for more details on how to choose the right strategy for your use-case and network.

type ClientIPStrategyFunc added in v0.14.0

type ClientIPStrategyFunc func(c Context) (*net.IPAddr, error)

The ClientIPStrategyFunc type is an adapter to allow the use of ordinary functions as ClientIPStrategy. If f is a function with the appropriate signature, ClientIPStrategyFunc(f) is a ClientIPStrategyFunc that calls f.

func (ClientIPStrategyFunc) ClientIP added in v0.14.0

func (f ClientIPStrategyFunc) ClientIP(c Context) (*net.IPAddr, error)

ClientIP calls f(c).

type Context added in v0.7.0

type Context interface {
	// Request returns the current *http.Request.
	Request() *http.Request
	// SetRequest sets the *http.Request.
	SetRequest(r *http.Request)
	// Writer method returns a custom ResponseWriter implementation.
	Writer() ResponseWriter
	// SetWriter sets the ResponseWriter.
	SetWriter(w ResponseWriter)
	// RemoteIP parses the IP from Request.RemoteAddr, normalizes it, and returns an IP address. The returned *net.IPAddr
	// may contain a zone identifier. RemoteIP never returns nil, even if parsing the IP fails.
	RemoteIP() *net.IPAddr
	// ClientIP returns the "real" client IP address based on the configured ClientIPStrategy.
	// The strategy is set using the WithClientIPStrategy option. There is no sane default, so if no strategy is configured,
	// the method returns ErrNoClientIPStrategy.
	//
	// The strategy used must be chosen and tuned for your network configuration. This should result
	// in the strategy never returning an error -- i.e., never failing to find a candidate for the "real" IP.
	// Consequently, getting an error result should be treated as an application error, perhaps even
	// worthy of panicking.
	//
	// The returned *net.IPAddr may contain a zone identifier.
	//
	// This api is EXPERIMENTAL and is likely to change in future release.
	ClientIP() (*net.IPAddr, error)
	// Path returns the registered path for the handler.
	Path() string
	// Params returns a Params slice containing the matched
	// wildcard parameters.
	Params() Params
	// Param retrieve a matching wildcard parameter by name.
	Param(name string) string
	// QueryParams parses the Request RawQuery and returns the corresponding values.
	QueryParams() url.Values
	// QueryParam returns the first query value associated with the given key.
	QueryParam(name string) string
	// SetHeader sets the response header for the given key to the specified value.
	SetHeader(key, value string)
	// Header retrieves the value of the request header for the given key.
	Header(key string) string
	// String sends a formatted string with the specified status code.
	String(code int, format string, values ...any) error
	// Blob sends a byte slice with the specified status code and content type.
	Blob(code int, contentType string, buf []byte) error
	// Stream sends data from an io.Reader with the specified status code and content type.
	Stream(code int, contentType string, r io.Reader) error
	// Redirect sends an HTTP redirect response with the given status code and URL.
	Redirect(code int, url string) error
	// Clone returns a copy of the Context that is safe to use after the HandlerFunc returns.
	Clone() Context
	// CloneWith returns a copy of the current Context, substituting its ResponseWriter and
	// http.Request with the provided ones. The method is designed for zero allocation during the
	// copy process. The returned ContextCloser must be closed once no longer needed.
	// This functionality is particularly beneficial for middlewares that need to wrap
	// their custom ResponseWriter while preserving the state of the original Context.
	CloneWith(w ResponseWriter, r *http.Request) ContextCloser
	// Tree is a local copy of the Tree in use to serve the request.
	Tree() *Tree
	// Fox returns the Router instance.
	Fox() *Router
	// Reset resets the Context to its initial state, attaching the provided ResponseWriter and http.Request.
	Reset(w ResponseWriter, r *http.Request)
}

Context represents the context of the current HTTP request. It provides methods to access request data and to write a response. Be aware that the Context API is not thread-safe and its lifetime should be limited to the duration of the HandlerFunc execution, as the underlying implementation may be reused a soon as the handler return. (see Clone method).

func NewTestContextOnly added in v0.7.0

func NewTestContextOnly(fox *Router, w http.ResponseWriter, r *http.Request) Context

NewTestContextOnly returns a new Context associated with the provided Router, designed only for testing purpose.

type ContextCloser added in v0.7.0

type ContextCloser interface {
	Context
	Close()
}

ContextCloser extends Context for manually created instances, adding a Close method to release resources after use.

type HandlerFunc

type HandlerFunc func(c Context)

HandlerFunc is a function type that responds to an HTTP request. It enforces the same contract as http.Handler but provides additional feature like matched wildcard route segments via the Context type. The Context is freed once the HandlerFunc returns and may be reused later to save resources. If you need to hold the context longer, you have to copy it (see Clone method).

Similar to http.Handler, to abort a HandlerFunc so the client sees an interrupted response, panic with the value http.ErrAbortHandler.

HandlerFunc functions should be thread-safe, as they will be called concurrently.

func DefaultMethodNotAllowedHandler added in v0.7.6

func DefaultMethodNotAllowedHandler() HandlerFunc

DefaultMethodNotAllowedHandler returns a simple HandlerFunc that replies to each request with a “405 Method Not Allowed” reply.

func DefaultNotFoundHandler added in v0.7.6

func DefaultNotFoundHandler() HandlerFunc

DefaultNotFoundHandler returns a simple HandlerFunc that replies to each request with a “404 page not found” reply.

func DefaultOptionsHandler added in v0.13.0

func DefaultOptionsHandler() HandlerFunc

DefaultOptionsHandler returns a simple HandlerFunc that replies to each request with a "200 OK" reply.

func WrapF

func WrapF(f http.HandlerFunc) HandlerFunc

WrapF is an adapter for wrapping http.HandlerFunc and returns a HandlerFunc function. The route parameters are being accessed by the wrapped handler through the context.

func WrapH

func WrapH(h http.Handler) HandlerFunc

WrapH is an adapter for wrapping http.Handler and returns a HandlerFunc function. The route parameters are being accessed by the wrapped handler through the context.

type Iterator added in v0.3.0

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

func NewIterator added in v0.5.0

func NewIterator(t *Tree) *Iterator

NewIterator returns an Iterator that traverses all registered routes in lexicographic order. An Iterator is safe to use when the router is serving request, when routing updates are ongoing or in parallel with other Iterators. Note that changes that happen while iterating over routes may not be reflected by the Iterator. This api is EXPERIMENTAL and is likely to change in future release.

Example
r := New()
it := NewIterator(r.Tree())

// Iterate over all routes
for it.Rewind(); it.Valid(); it.Next() {
	fmt.Println(it.Method(), it.Path())
}

// Iterate over all routes for the GET method
for it.SeekMethod(http.MethodGet); it.Valid(); it.Next() {
	fmt.Println(it.Method(), it.Path())
}

// Iterate over all routes starting with /users
for it.SeekPrefix("/users"); it.Valid(); it.Next() {
	fmt.Println(it.Method(), it.Path())
}

// Iterate over all route starting with /users for the GET method
for it.SeekMethodPrefix(http.MethodGet, "/user"); it.Valid(); it.Next() {
	fmt.Println(it.Method(), it.Path())
}
Output:

func (*Iterator) Handler added in v0.3.0

func (it *Iterator) Handler() HandlerFunc

Handler return the registered handler for the current route.

func (*Iterator) Method added in v0.3.0

func (it *Iterator) Method() string

Method returns the http method for the current route.

func (*Iterator) Next added in v0.3.0

func (it *Iterator) Next()

Next advance the iterator to the next route. Always check it.Valid() after a it.Next().

func (*Iterator) Path added in v0.3.0

func (it *Iterator) Path() string

Path returns the registered path for the current route.

func (*Iterator) Rewind added in v0.3.0

func (it *Iterator) Rewind()

Rewind reset the iterator cursor all the way to zero-th position which is the first method and route. It does not keep track of whether the cursor started with SeekPrefix, SeekMethod or SeekMethodPrefix.

func (*Iterator) SeekMethod added in v0.3.0

func (it *Iterator) SeekMethod(method string)

SeekMethod reset the iterator cursor to the first route for the given method. It does not keep tracking of previous seek.

func (*Iterator) SeekMethodPrefix added in v0.3.0

func (it *Iterator) SeekMethodPrefix(method, key string)

SeekMethodPrefix reset the iterator cursor to the first route starting with key for the given method. It does not keep tracking of previous seek.

func (*Iterator) SeekPrefix added in v0.3.0

func (it *Iterator) SeekPrefix(key string)

SeekPrefix reset the iterator cursor to the first route starting with key. It does not keep tracking of previous seek.

func (*Iterator) Valid added in v0.3.0

func (it *Iterator) Valid() bool

Valid returns false when iteration is done.

type MiddlewareFunc added in v0.7.0

type MiddlewareFunc func(next HandlerFunc) HandlerFunc

MiddlewareFunc is a function type for implementing HandlerFunc middleware. The returned HandlerFunc usually wraps the input HandlerFunc, allowing you to perform operations before and/or after the wrapped HandlerFunc is executed. MiddlewareFunc functions should be thread-safe, as they will be called concurrently.

func CustomRecovery added in v0.14.0

func CustomRecovery(handle RecoveryFunc) MiddlewareFunc

CustomRecovery returns a middleware that recovers from any panics, logs the error, request details, and stack trace, and then calls the provided handle function to handle the recovery.

func CustomRecoveryWithLogHandler added in v0.14.0

func CustomRecoveryWithLogHandler(handler slog.Handler, handle RecoveryFunc) MiddlewareFunc

CustomRecoveryWithLogHandler returns a middleware for a given slog.Handler that recovers from any panics, logs the error, request details, and stack trace, and then calls the provided handle function to handle the recovery.

func Logger added in v0.14.0

func Logger() MiddlewareFunc

Logger returns a middleware that logs request information to os.Stdout or os.Stderr (for ERROR level). It logs details such as the remote or client IP, HTTP method, request path, status code and latency.

func LoggerWithHandler added in v0.14.0

func LoggerWithHandler(handler slog.Handler) MiddlewareFunc

LoggerWithHandler returns a middleware that logs request information using the provided slog.Handler. It logs details such as the remote or client IP, HTTP method, request path, status code and latency.

func Recovery added in v0.7.0

func Recovery() MiddlewareFunc

Recovery returns a middleware that recovers from any panics, logs the error, request details, and stack trace, and writes a 500 status code response if a panic occurs.

type MiddlewareScope added in v0.7.6

type MiddlewareScope uint8

MiddlewareScope is a type that represents different scopes for applying middleware.

const (
	// RouteHandlers scope applies middleware only to regular routes registered in the router.
	RouteHandlers MiddlewareScope = 1 << (8 - 1 - iota)
	// NoRouteHandler scope applies middleware to the NoRoute handler.
	NoRouteHandler
	// NoMethodHandler scope applies middleware to the NoMethod handler.
	NoMethodHandler
	// RedirectHandler scope applies middleware to the internal redirect trailing slash handler.
	RedirectHandler
	// OptionsHandler scope applies middleware to the automatic OPTIONS handler.
	OptionsHandler
	// AllHandlers is a combination of all the above scopes, which means the middleware will be applied to all types of handlers.
	AllHandlers = RouteHandlers | NoRouteHandler | NoMethodHandler | RedirectHandler | OptionsHandler
)

type Option added in v0.5.0

type Option interface {
	// contains filtered or unexported methods
}

func DefaultOptions added in v0.7.0

func DefaultOptions() Option

DefaultOptions configure the router to use the Recovery middleware for the RouteHandlers scope, the Logger middleware for AllHandlers scope and enable automatic OPTIONS response. Note that DefaultOptions push the Recovery and Logger middleware respectively to the first and second position of the middleware chains.

func WithAutoOptions added in v0.9.0

func WithAutoOptions(enable bool) Option

WithAutoOptions enables automatic response to OPTIONS requests with, by default, a 200 OK status code. Use the WithOptionsHandler option to customize the response. When this option is enabled, the router automatically determines the "Allow" header value based on the methods registered for the given route. Note that custom OPTIONS handler take priority over automatic replies. This option is automatically enabled when providing a custom handler with the option WithOptionsHandler. This api is EXPERIMENTAL and is likely to change in future release.

func WithClientIPStrategy added in v0.14.0

func WithClientIPStrategy(strategy ClientIPStrategy) Option

WithClientIPStrategy sets the strategy for obtaining the "real" client IP address from HTTP requests. This strategy is used by the Context.ClientIP method. The strategy must be chosen and tuned for your network configuration to ensure it never returns an error -- i.e., never fails to find a candidate for the "real" IP. Consequently, getting an error result should be treated as an application error, perhaps even worthy of panicking. There is no sane default, so if no strategy is configured, Context.ClientIP returns ErrNoClientIPStrategy. This API is EXPERIMENTAL and is likely to change in future releases.

func WithIgnoreTrailingSlash added in v0.14.0

func WithIgnoreTrailingSlash(enable bool) Option

WithIgnoreTrailingSlash allows the router to match routes regardless of whether a trailing slash is present or not. E.g. /foo/bar/ and /foo/bar would both match the same handler. This option prevents the router from issuing a redirect and instead matches the request directly. Note that this option is mutually exclusive with WithRedirectTrailingSlash, and if both are enabled, WithIgnoreTrailingSlash takes precedence. This api is EXPERIMENTAL and is likely to change in future release.

func WithMiddleware added in v0.7.0

func WithMiddleware(m ...MiddlewareFunc) Option

WithMiddleware attaches a global middleware to the router. Middlewares provided will be chained in the order they were added. Note that this option apply middleware to all handler, including NotFound, MethodNotAllowed and the internal redirect handler.

Example

This example demonstrates how to register a global middleware that will be applied to all routes.

// Define a custom middleware to measure the time taken for request processing and
// log the URL, route, time elapsed, and status code.
metrics := func(next HandlerFunc) HandlerFunc {
	return func(c Context) {
		start := time.Now()
		next(c)
		log.Printf(
			"url=%s; route=%s; time=%d; status=%d",
			c.Request().URL,
			c.Path(),
			time.Since(start),
			c.Writer().Status(),
		)
	}
}

f := New(WithMiddleware(metrics))

f.MustHandle(http.MethodGet, "/hello/{name}", func(c Context) {
	_ = c.String(200, "Hello %s\n", c.Param("name"))
})
Output:

func WithMiddlewareFor added in v0.7.6

func WithMiddlewareFor(scope MiddlewareScope, m ...MiddlewareFunc) Option

WithMiddlewareFor attaches middleware to the router for a specified scope. Middlewares provided will be chained in the order they were added. The scope parameter determines which types of handlers the middleware will be applied to. Possible scopes include RouteHandlers (regular routes), NoRouteHandler, NoMethodHandler, RedirectHandler, OptionsHandler, and any combination of these. Use this option when you need fine-grained control over where the middleware is applied. This api is EXPERIMENTAL and is likely to change in future release.

func WithNoMethod added in v0.9.0

func WithNoMethod(enable bool) Option

WithNoMethod enable to returns 405 Method Not Allowed instead of 404 Not Found when the route exist for another http verb. The "Allow" header it automatically set before calling the handler. Note that this option is automatically enabled when providing a custom handler with the option WithNoMethodHandler.

func WithNoMethodHandler added in v0.9.0

func WithNoMethodHandler(handler HandlerFunc) Option

WithNoMethodHandler register an HandlerFunc which is called when the request cannot be routed, but the same route exist for other methods. The "Allow" header it automatically set before calling the handler. By default, the DefaultMethodNotAllowedHandler is used. Note that this option automatically enable WithNoMethod.

func WithNoRouteHandler added in v0.9.0

func WithNoRouteHandler(handler HandlerFunc) Option

WithNoRouteHandler register an HandlerFunc which is called when no matching route is found. By default, the DefaultNotFoundHandler is used.

func WithOptionsHandler added in v0.9.0

func WithOptionsHandler(handler HandlerFunc) Option

WithOptionsHandler register an HandlerFunc which is called on automatic OPTIONS requests. By default, the router respond with a 200 OK status code. The "Allow" header it automatically set before calling the handler. Note that custom OPTIONS handler take priority over automatic replies. By default, DefaultOptionsHandler is used. Note that this option automatically enable WithAutoOptions. This api is EXPERIMENTAL and is likely to change in future release.

func WithRedirectTrailingSlash added in v0.5.0

func WithRedirectTrailingSlash(enable bool) Option

WithRedirectTrailingSlash enable automatic redirection fallback when the current request does not match but another handler is found with/without an additional trailing slash. E.g. /foo/bar/ request does not match but /foo/bar would match. The client is redirected with a http status code 301 for GET requests and 308 for all other methods. Note that this option is mutually exclusive with WithIgnoreTrailingSlash, and if both are enabled, WithIgnoreTrailingSlash takes precedence.

type Param

type Param struct {
	Key   string
	Value string
}

type Params

type Params []Param

func ParamsFromContext

func ParamsFromContext(ctx context.Context) Params

ParamsFromContext is a helper to retrieve params from context.Context when a http.Handler is registered using WrapF or WrapH.

func (Params) Clone

func (p Params) Clone() Params

Clone make a copy of Params.

func (Params) Get

func (p Params) Get(name string) string

Get the matching wildcard segment by name.

func (Params) Has added in v0.9.2

func (p Params) Has(name string) bool

Has checks whether the parameter exists by name.

type RecoveryFunc added in v0.7.0

type RecoveryFunc func(c Context, err any)

RecoveryFunc is a function type that defines how to handle panics that occur during the handling of an HTTP request.

type ResponseWriter added in v0.7.0

type ResponseWriter interface {
	http.ResponseWriter
	io.StringWriter
	io.ReaderFrom
	// Status recorded after Write and WriteHeader.
	Status() int
	// Written returns true if the response has been written.
	Written() bool
	// Size returns the size of the written response.
	Size() int
	// FlushError flushes buffered data to the client. If flush is not supported, FlushError returns an error
	// matching http.ErrNotSupported. See http.Flusher for more details.
	FlushError() error
	// Hijack lets the caller take over the connection. If hijacking the connection is not supported, Hijack returns
	// an error matching http.ErrNotSupported. See http.Hijacker for more details.
	Hijack() (net.Conn, *bufio.ReadWriter, error)
	// Push initiates an HTTP/2 server push. Push returns http.ErrNotSupported if the client has disabled push or if push
	// is not supported on the underlying connection. See http.Pusher for more details.
	Push(target string, opts *http.PushOptions) error
}

ResponseWriter extends http.ResponseWriter and provides methods to retrieve the recorded status code, written state, and response size.

type RouteConflictError

type RouteConflictError struct {
	Method  string
	Path    string
	Matched []string
	// contains filtered or unexported fields
}

RouteConflictError is a custom error type used to represent conflicts when registering or updating routes in the router. It holds information about the conflicting method, path, and the matched routes that caused the conflict.

func (*RouteConflictError) Error

func (e *RouteConflictError) Error() string

Error returns a formatted error message for the RouteConflictError.

func (*RouteConflictError) Unwrap

func (e *RouteConflictError) Unwrap() error

Unwrap returns the sentinel value ErrRouteConflict.

type Router

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

Router is a lightweight high performance HTTP request router that support mutation on its routing tree while handling request concurrently.

func New

func New(opts ...Option) *Router

New returns a ready to use instance of Fox router.

Example

This example demonstrates how to create a simple router using the default options, which include the Recovery and Logger middleware. A basic route is defined, along with a custom middleware to log the request metrics.

// Create a new router with default options, which include the Recovery and Logger middleware
r := New(DefaultOptions())

// Define a custom middleware to measure the time taken for request processing and
// log the URL, route, time elapsed, and status code.
metrics := func(next HandlerFunc) HandlerFunc {
	return func(c Context) {
		start := time.Now()
		next(c)
		log.Printf("url=%s; route=%s; time=%d; status=%d", c.Request().URL, c.Path(), time.Since(start), c.Writer().Status())
	}
}

// Define a route with the path "/hello/{name}", apply the custom "metrics" middleware,
// and set a simple handler that greets the user by their name.
r.MustHandle(http.MethodGet, "/hello/{name}", metrics(func(c Context) {
	_ = c.String(200, "Hello %s\n", c.Param("name"))
}))

// Start the HTTP server using the router as the handler and listen on port 8080
log.Fatalln(http.ListenAndServe(":8080", r))
Output:

func (*Router) AutoOptionsEnabled added in v0.14.0

func (fox *Router) AutoOptionsEnabled() bool

AutoOptionsEnabled returns whether the router is configured to automatically respond to OPTIONS requests. This api is EXPERIMENTAL and is likely to change in future release.

func (*Router) ClientIPStrategyEnabled added in v0.14.0

func (fox *Router) ClientIPStrategyEnabled() bool

ClientIPStrategyEnabled returns whether the router is configured with a ClientIPStrategy. This api is EXPERIMENTAL and is likely to change in future release.

func (*Router) Handle added in v0.7.0

func (fox *Router) Handle(method, path string, handler HandlerFunc) error

Handle registers a new handler for the given method and path. This function return an error if the route is already registered or conflict with another. It's perfectly safe to add a new handler while the tree is in use for serving requests. This function is safe for concurrent use by multiple goroutine. To override an existing route, use Update.

func (*Router) IgnoreTrailingSlashEnabled added in v0.14.0

func (fox *Router) IgnoreTrailingSlashEnabled() bool

IgnoreTrailingSlashEnabled returns whether the router is configured to ignore trailing slashes in requests when matching routes. This api is EXPERIMENTAL and is likely to change in future release.

func (*Router) Lookup

func (fox *Router) Lookup(w ResponseWriter, r *http.Request) (handler HandlerFunc, cc ContextCloser, tsr bool)

Lookup is a helper that calls Tree.Lookup. For more details, refer to Tree.Lookup. It performs a manual route lookup for a given http.Request, returning the matched HandlerFunc along with a ContextCloser, and a boolean indicating if a trailing slash action (e.g. redirect) is recommended (tsr). The ContextCloser should always be closed if non-nil. This API is EXPERIMENTAL and is likely to change in future release.

Example

This example demonstrates how to create a custom middleware that cleans the request path and performs a manual lookup on the tree. If the cleaned path matches a registered route, the client is redirected to the valid path.

redirectFixedPath := MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
	return func(c Context) {
		req := c.Request()
		target := req.URL.Path
		cleanedPath := CleanPath(target)

		// Nothing to clean, call next handler or middleware.
		if cleanedPath == target {
			next(c)
			return
		}

		req.URL.Path = cleanedPath
		handler, cc, tsr := c.Fox().Lookup(c.Writer(), req)
		if handler != nil {
			defer cc.Close()

			code := http.StatusMovedPermanently
			if req.Method != http.MethodGet {
				code = http.StatusPermanentRedirect
			}

			// Redirect the client if direct match or indirect match.
			if !tsr || c.Fox().IgnoreTrailingSlashEnabled() {
				if err := c.Redirect(code, cleanedPath); err != nil {
					// Only if not in the range 300..308, so not possible here!
					panic(err)
				}
				return
			}

			// Add or remove an extra trailing slash and redirect the client.
			if c.Fox().RedirectTrailingSlashEnabled() {
				if err := c.Redirect(code, fixTrailingSlash(cleanedPath)); err != nil {
					// Only if not in the range 300..308, so not possible here
					panic(err)
				}
				return
			}
		}

		// rollback to the original path before calling the
		// next handler or middleware.
		req.URL.Path = target
		next(c)
	}
})

f := New(
	// Register the middleware for the NoRouteHandler scope.
	WithMiddlewareFor(NoRouteHandler|NoMethodHandler, redirectFixedPath),
)

f.MustHandle(http.MethodGet, "/hello/{name}", func(c Context) {
	_ = c.String(200, "Hello %s\n", c.Param("name"))
})
Output:

func (*Router) MethodNotAllowedEnabled added in v0.14.0

func (fox *Router) MethodNotAllowedEnabled() bool

MethodNotAllowedEnabled returns whether the router is configured to handle requests with methods that are not allowed. This api is EXPERIMENTAL and is likely to change in future release.

func (*Router) MustHandle added in v0.7.0

func (fox *Router) MustHandle(method, path string, handler HandlerFunc)

MustHandle registers a new handler for the given method and path. This function is a convenience wrapper for the Handle function. It will panic if the route is already registered or conflicts with another route. It's perfectly safe to add a new handler while the tree is in use for serving requests. This function is safe for concurrent use by multiple goroutines. To override an existing route, use Update.

func (*Router) NewTree added in v0.5.0

func (fox *Router) NewTree() *Tree

NewTree returns a fresh routing Tree that inherits all registered router options. It's safe to create multiple Tree concurrently. However, a Tree itself is not thread-safe and all its APIs that perform write operations should be run serially. Note that a Tree give direct access to the underlying sync.Mutex. This API is EXPERIMENTAL and is likely to change in future release.

func (*Router) RedirectTrailingSlashEnabled added in v0.14.0

func (fox *Router) RedirectTrailingSlashEnabled() bool

RedirectTrailingSlashEnabled returns whether the router is configured to automatically redirect requests that include or omit a trailing slash. This api is EXPERIMENTAL and is likely to change in future release.

func (*Router) Remove

func (fox *Router) Remove(method, path string) error

Remove delete an existing handler for the given method and path. If the route does not exist, the function return an ErrRouteNotFound. It's perfectly safe to remove a handler while the tree is in use for serving requests. This function is safe for concurrent use by multiple goroutine.

func (*Router) ServeHTTP

func (fox *Router) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP is the main entry point to serve a request. It handles all incoming HTTP requests and dispatches them to the appropriate handler function based on the request's method and path.

func (*Router) Swap added in v0.5.0

func (fox *Router) Swap(new *Tree) (old *Tree)

Swap atomically replaces the currently in-use routing tree with the provided new tree, and returns the previous tree. This API is EXPERIMENTAL and is likely to change in future release.

func (*Router) Tree added in v0.5.0

func (fox *Router) Tree() *Tree

Tree atomically loads and return the currently in-use routing tree. This API is EXPERIMENTAL and is likely to change in future release.

Example

This example demonstrates some important considerations when using the Tree API.

r := New()

// Each tree as its own sync.Mutex that is used to lock write on the tree. Since the router tree may be swapped at
// any given time, you MUST always copy the pointer locally, This ensures that you do not inadvertently cause a
// deadlock by locking/unlocking the wrong tree.
tree := r.Tree()
upsert := func(method, path string, handler HandlerFunc) error {
	tree.Lock()
	defer tree.Unlock()
	if tree.Has(method, path) {
		return tree.Update(method, path, handler)
	}
	return tree.Handle(method, path, handler)
}

_ = upsert(http.MethodGet, "/foo/bar", func(c Context) {
	// Note the tree accessible from fox.Context is already a local copy so the golden rule above does not apply.
	c.Tree().Lock()
	defer c.Tree().Unlock()
	_ = c.String(200, "foo bar")
})

// Bad, instead make a local copy of the tree!
_ = func(method, path string, handler HandlerFunc) error {
	r.Tree().Lock()
	defer r.Tree().Unlock()
	if r.Tree().Has(method, path) {
		return r.Tree().Update(method, path, handler)
	}
	return r.Tree().Handle(method, path, handler)
}
Output:

func (*Router) Update

func (fox *Router) Update(method, path string, handler HandlerFunc) error

Update override an existing handler for the given method and path. If the route does not exist, the function return an ErrRouteNotFound. It's perfectly safe to update a handler while the tree is in use for serving requests. This function is safe for concurrent use by multiple goroutine. To add new handler, use Handle method.

type Tree added in v0.5.0

type Tree struct {
	sync.Mutex
	// contains filtered or unexported fields
}

Tree implements a Concurrent Radix Tree that supports lock-free reads while allowing concurrent writes. The caller is responsible for ensuring that all writes are run serially.

IMPORTANT: Each tree as its own sync.Mutex that may be used to serialize write. Since the router tree may be swapped at any given time, you MUST always copy the pointer locally to avoid inadvertently causing a deadlock by locking/unlocking the wrong Tree.

Good: t := fox.Tree() t.Lock() defer t.Unlock()

Dramatically bad, may cause deadlock fox.Tree().Lock() defer fox.Tree().Unlock()

func (*Tree) Handle added in v0.7.0

func (t *Tree) Handle(method, path string, handler HandlerFunc) error

Handle registers a new handler for the given method and path. This function return an error if the route is already registered or conflict with another. It's perfectly safe to add a new handler while the tree is in use for serving requests. However, this function is NOT thread-safe and should be run serially, along with all other Tree APIs that perform write operations. To override an existing route, use Update.

func (*Tree) Has added in v0.8.0

func (t *Tree) Has(method, path string) bool

Has allows to check if the given method and path exactly match a registered route. This function is safe for concurrent use by multiple goroutine and while mutation on Tree are ongoing. This API is EXPERIMENTAL and is likely to change in future release.

Example

This example demonstrates how to check if a given route is registered in the tree.

f := New()
f.MustHandle(http.MethodGet, "/hello/{name}", emptyHandler)

tree := f.Tree()
exist := tree.Match(http.MethodGet, "/hello/{name}")
fmt.Println(exist) // true
Output:

func (*Tree) Lookup added in v0.8.0

func (t *Tree) Lookup(w ResponseWriter, r *http.Request) (handler HandlerFunc, cc ContextCloser, tsr bool)

Lookup performs a manual route lookup for a given http.Request, returning the matched HandlerFunc along with a ContextCloser, and a boolean indicating if the handler was matched by adding or removing a trailing slash (trailing slash action is recommended). The ContextCloser should always be closed if non-nil. This method is primarily intended for integrating the fox router into custom routing solutions or middleware. This function is safe for concurrent use by multiple goroutine and while mutation on Tree are ongoing. If there is a direct match or a tsr is possible, Lookup always return a HandlerFunc and a ContextCloser. This API is EXPERIMENTAL and is likely to change in future release.

func (*Tree) Match added in v0.13.0

func (t *Tree) Match(method, path string) string

Match perform a reverse lookup on the tree for the given method and path and return the matching registered route if any. When WithIgnoreTrailingSlash or WithRedirectTrailingSlash are enabled, Match will match a registered route regardless of an extra or missing trailing slash. This function is safe for concurrent use by multiple goroutine and while mutation on Tree are ongoing. See also Tree.Lookup as an alternative. This API is EXPERIMENTAL and is likely to change in future release.

Example

This example demonstrates how to do a reverse lookup on the tree.

f := New()
f.MustHandle(http.MethodGet, "/hello/{name}", emptyHandler)

tree := f.Tree()
matched := tree.Match(http.MethodGet, "/hello/fox")
fmt.Println(matched) // /hello/{name}
Output:

func (*Tree) Methods added in v0.7.4

func (t *Tree) Methods(path string) []string

Methods returns a sorted list of HTTP methods associated with a given path in the routing tree. If the path is "*", it returns all HTTP methods that have at least one route registered in the tree. For a specific path, it returns the methods that can route requests to that path. When WithIgnoreTrailingSlash or WithRedirectTrailingSlash are enabled, Methods will match a registered route regardless of an extra or missing trailing slash. This function is safe for concurrent use by multiple goroutine and while mutation on Tree are ongoing. This API is EXPERIMENTAL and is likely to change in future release.

func (*Tree) Remove added in v0.5.0

func (t *Tree) Remove(method, path string) error

Remove delete an existing handler for the given method and path. If the route does not exist, the function return an ErrRouteNotFound. It's perfectly safe to remove a handler while the tree is in use for serving requests. However, this function is NOT thread-safe and should be run serially, along with all other Tree APIs that perform write operations.

func (*Tree) Update added in v0.5.0

func (t *Tree) Update(method, path string, handler HandlerFunc) error

Update override an existing handler for the given method and path. If the route does not exist, the function return an ErrRouteNotFound. It's perfectly safe to update a handler while the tree is in use for serving requests. However, this function is NOT thread-safe and should be run serially, along with all other Tree APIs that perform write operations. To add a new handler, use Handle method.

type WalkFunc

type WalkFunc func(method, path string, handler HandlerFunc) error

WalkFunc is the type of the function called by Walk to visit each registered routes.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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