fox

package module
v0.22.1 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2025 License: Apache-2.0 Imports: 30 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 Radix Tree that support lock-free reads while allowing concurrent write. 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 frequent 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 delete 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 segment can then be easily retrieved by name. Due to Fox design, wildcard route are cheap and scale really well.

Hostname matching: Fox supports hostname-based routing with wildcard matching.

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.

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.

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

Rich middleware ecosystem: Fox offers a robust ecosystem of prebuilt, high-quality middlewares, ready to integrate into your application.

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



Getting started

Install

With a correctly configured Go toolchain:

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

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

func main() {
	f, err := fox.New(fox.DefaultOptions())
	if err != nil {
		panic(err)
	}

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

	if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatalln(err)
	}
}
Route Validation and 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:

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

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

Routes can include named parameters using curly braces {} to match exactly one non-empty route segment. The matching segment are recorder into fox.Param accessible via fox.Context. fox.Context.Params provide an iterator to range over fox.Param and fox.Context.Param allow to retrieve directly the value of a parameter using the placeholder name. 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

Named parameters are also supported in hostname. Note that the path portion must still include at least /.

Pattern example.com/avengers

example.com/avengers            matches
{sub}.com/avengers              matches
example.{tld}/avengers          matches
{sub}.example.com/avengers      no matches
Catch-all parameters

Catch-all parameters start with an asterisk * followed by a name {param} and match one or more non-empty route segments, including slashes. The matching segment are also accessible via fox.Context. Catch-all parameters are supported anywhere in the route, but only one parameter is allowed per segment and must appear at the end of the segment. Consecutive catch-all parameter are not allowed.

Example with ending catch all

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

Example with infix catch all

Pattern: /assets/*{path}/thumbnail

/assets/images/thumbnail            matches
/assets/photos/2021/thumbnail       matches
/assets/thumbnail                   no matches

Pattern: /assets/path:*{path}/thumbnail

/assets/path:images/thumbnail       matches
/assets/path:photos/2021/thumbnail  matches
/assets/path:thumbnail              no matches

Note that catch-all patterns are not supported in hostname.

Hostname validation & restrictions

Hostnames are validated to conform to the LDH (letters, digits, hyphens) rule. Wildcard segments within hostnames, such as {a}.b.c/, 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 255-character limit for the full hostname (including periods). Internationalized domain names (IDNs) should be specified using an ASCII (Punycode) representation.

The DNS specification permits a trailing period to be used to denote the root, e.g., a.b.c and a.b.c. are equivalent, but the latter is more explicit and is required to be accepted by applications. Fox will reject route registered with trailing period. However, the router will automatically strip any trailing period from incoming request host so it can match the route regardless of a trailing period. Note that FQDN (with trailing period) does not play well with golang TLS stdlib (see traefik/traefik#9157 (comment)).

Priority rules

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

The following rules apply:

  • Routes with hostnames are evaluated first, before any path-only routes.
  • If no route matches with a hostname, the router falls back to matching path-only routes. Path-only routes match requests with any hostname.
  • 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:

Route Definitions:

1. GET /fs/avengers.txt          # Highest priority (static)
2. GET /fs/{filename}            # Next priority (named parameter)
3. GET /fs/*{filepath}           # Lowest priority (catch-all parameter)

Request Matching:

- /fs/avengers.txt              matches Route 1
- /fs/ironman.txt               matches Route 2
- /fs/avengers/ironman.txt      matches Route 3
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, ensuring optimal and adaptive routing behavior.

  • If the routing tree for a given method has no routes registered with hostnames, the router will perform a path-based lookup only.
  • If the routing tree for a given method 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.
  • Trailing slash handling (redirect or ignore) is mode-specific, either for hostname-prioritized or path-prioritized mode. Therefore, if no exact match is found for a domain-based lookup but a trailing slash adjustment is possible, Fox will perform the redirect (or ignore the trailing slash) without falling back to path-only lookup.
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.

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 that supports uncoordinated read while allowing a single writer to make progress. Updates are applied by calculating the change which would be made to the tree were it mutable, assembling those changes into a patch which is propagated to the root and applied in a single atomic operation. The result is a shallow copy of the tree, where only the modified path and its ancestors are cloned, ensuring efficient updates and minimal memory overhead. Multiple patches can be applied in a single transaction, with intermediate nodes cached during the process to prevent redundant cloning.

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 delete 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"
	"errors"
	"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().Delete(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, err := fox.New(fox.DefaultOptions())
	if err != nil {
		panic(err)
	}

	f.MustHandle(http.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
// 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.Handle(http.MethodGet, "exemple.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 Prefix (or any other iterator).
	for method, route := range it.Prefix(it.Methods(), "tmp.exemple.com/") {
		if _, err := txn.Delete(method, route.Pattern()); err != nil {
			return err
		}
	}
	return nil
}); err != nil {
	log.Printf("transaction aborted: %s", err)
}
Unmanaged read-write transaction
// Txn create an unmanaged read-write or read-only transaction.
txn := f.Txn(true)
defer txn.Abort()

if _, err := txn.Handle(http.MethodGet, "exemple.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 Prefix (or any other iterator).
for method, route := range it.Prefix(it.Methods(), "tmp.exemple.com/") {
	if _, err := txn.Delete(method, route.Pattern()); err != nil {
		log.Printf("error deleting route: %s", err)
		return
	}
}
// Finalize the transaction
txn.Commit()
Managed read-only transaction
_ = f.View(func(txn *fox.Txn) error {
	if txn.Has(http.MethodGet, "/foo") {
		if txn.Has(http.MethodGet, "/bar") {
			// do something
		}
	}
	return nil
})

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

func main() {
	f, err := fox.New(fox.WithMiddleware(Logger))
	if err != nil {
		panic(err)
	}

	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.WithMiddlewareFor(fox.RouteHandler, fox.Recovery(), 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).

f, _ := fox.New(
	fox.WithMiddleware(fox.Logger()),
)
f.MustHandle("GET", "/", SomeHandler, fox.WithMiddleware(foxtimeout.Middleware(2*time.Second)))
f.MustHandle("GET", "/foo", SomeOtherHandler)
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)
    }),
)

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/tigerwill90/fox/clientip provides a set of best practices resolvers that should cover most use cases.

package main

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

func main() {
	resolver, err := clientip.NewRightmostNonPrivate(clientip.XForwardedForKey)
	if err != nil {
		panic(err)
	}
	f, err := fox.New(
		fox.DefaultOptions(),
		fox.WithClientIPResolver(
			resolver,
		),
	)
	if err != nil {
		panic(err)
	}

	f.MustHandle(http.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.

resolver, _ := clientip.NewLeftmostNonPrivate(clientip.ForwardedKey, 10)
f, _ = fox.New(
	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 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               = "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"
	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"
	HeaderHost                = "Host"
	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"
	HeaderXRealIP              = "X-Real-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")
	ErrNoClientIPResolver      = errors.New("no client ip resolver")
	ErrReadOnlyTxn             = errors.New("write on read-only transaction")
	ErrSettledTxn              = errors.New("transaction settled")
	ErrParamKeyTooLarge        = errors.New("parameter key too large")
	ErrTooManyParams           = errors.New("too many params")
	ErrInvalidConfig           = errors.New("invalid config")
)

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 DefaultMethodNotAllowedHandler added in v0.7.6

func DefaultMethodNotAllowedHandler(c Context)

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

func DefaultNotFoundHandler added in v0.7.6

func DefaultNotFoundHandler(c Context)

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

func DefaultOptionsHandler added in v0.13.0

func DefaultOptionsHandler(c Context)

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

func ErrNotSupported added in v0.16.0

func ErrNotSupported() error

ErrNotSupported returns an error that Is http.ErrNotSupported, but is not == to it.

func FixTrailingSlash added in v0.16.0

func FixTrailingSlash(path string) string

FixTrailingSlash ensures a consistent trailing slash handling for a given path. If the path has more than one character and ends with a slash, it removes the trailing slash. Otherwise, it adds a trailing slash to the path.

func NewTestContext added in v0.7.0

func NewTestContext(w http.ResponseWriter, r *http.Request, opts ...GlobalOption) (*Router, *TestContext)

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

func SplitHostPath added in v0.18.0

func SplitHostPath(url string) (host, path string)

SplitHostPath separates the host and path from a URL string. If url includes a valid numeric port, the port is stripped from the host; otherwise, it remains part of the host. If url is empty or lacks a path, the path defaults to "/". SplitHostPath does not perform host validation.

Types

type ClientIPResolver added in v0.19.0

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

ClientIPResolver define a resolver for obtaining the "real" client IP from HTTP requests. The resolver used must be chosen and tuned for your network configuration. This should result in a resolver 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 resolver can be found in the github.com/tigerwill90/fox/clientip package.

type ClientIPResolverFunc added in v0.19.0

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

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

func (ClientIPResolverFunc) ClientIP added in v0.19.0

func (f ClientIPResolverFunc) 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 [http.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 [ClientIPResolver].
	// The resolver is set using the [WithClientIPResolver] option. There is no sane default, so if no resolver is configured,
	// the method returns [ErrNoClientIPResolver].
	//
	// The resolver used must be chosen and tuned for your network configuration. This should result
	// in a resolver 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.
	ClientIP() (*net.IPAddr, error)
	// Pattern returns the registered route pattern or an empty string if the handler is called in a scope other than [RouteHandler].
	Pattern() string
	// Route returns the registered [Route] or nil if the handler is called in a scope other than [RouteHandler].
	Route() *Route
	// Params returns a range iterator over the matched wildcard parameters for the current route.
	Params() iter.Seq[Param]
	// Param retrieve a matching wildcard parameter by name.
	Param(name string) string
	// Method returns the request method.
	Method() string
	// Path returns the request URL path.
	Path() string
	// Host returns the request host.
	Host() string
	// QueryParams parses the [http.Request] raw query 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)
	// AddHeader add the response header for the given key to the specified value.
	AddHeader(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 shallow 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
	// Scope returns the [HandlerScope] associated with the current [Context].
	// This indicates the scope in which the handler is being executed, such as [RouteHandler], [NoRouteHandler], etc.
	Scope() HandlerScope
	// Fox returns the [Router] instance.
	Fox() *Router
}

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 [Context.Clone] method).

type ContextCloser added in v0.7.0

type ContextCloser interface {
	Context
	// Close releases the context to be reused later.
	Close()
}

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

type GlobalOption added in v0.16.0

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

func DefaultOptions added in v0.7.0

func DefaultOptions() GlobalOption

DefaultOptions configure the router to use the Recovery middleware for the RouteHandler 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) GlobalOption

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.

func WithMaxRouteParamKeyBytes added in v0.19.0

func WithMaxRouteParamKeyBytes(max uint16) GlobalOption

WithMaxRouteParamKeyBytes set the maximum number of bytes allowed per parameter key in a route. The default max is math.MaxUint16.

func WithMaxRouteParams added in v0.19.0

func WithMaxRouteParams(max uint16) GlobalOption

WithMaxRouteParams set the maximum number of parameters allowed in a route. The default max is math.MaxUint16.

func WithMiddlewareFor added in v0.7.6

func WithMiddlewareFor(scope HandlerScope, m ...MiddlewareFunc) GlobalOption

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 RouteHandler (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.

func WithNoMethod added in v0.9.0

func WithNoMethod(enable bool) GlobalOption

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

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

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

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.

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 [Context.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 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 HandlerScope added in v0.17.0

type HandlerScope uint8

HandlerScope represents different scopes where a handler may be called. It also allows for fine-grained control over where middleware is applied.

const (
	// RouteHandler scope applies to regular routes registered in the router.
	RouteHandler HandlerScope = 1 << (8 - 1 - iota)
	// NoRouteHandler scope applies to the NoRoute handler, which is invoked when no route matches the request.
	NoRouteHandler
	// NoMethodHandler scope applies to the NoMethod handler, which is invoked when a route exists, but the method is not allowed.
	NoMethodHandler
	// RedirectHandler scope applies to the internal redirect handler, used for handling requests with trailing slashes.
	RedirectHandler
	// OptionsHandler scope applies to the automatic OPTIONS handler, which handles pre-flight or cross-origin requests.
	OptionsHandler
	// AllHandlers is a combination of all the above scopes, which can be used to apply middlewares to all types of handlers.
	AllHandlers = RouteHandler | NoRouteHandler | NoMethodHandler | RedirectHandler | OptionsHandler
)

type Iter added in v0.16.0

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

Iter provide a set of range iterators for traversing registered methods and routes. Iter capture a point-in-time snapshot of the routing tree. Therefore, all iterators returned by Iter will not observe subsequent write on the router or on the transaction from which the Iter is created.

func (Iter) All added in v0.16.0

func (it Iter) All() iter.Seq2[string, *Route]

All returns a range iterator over all routes registered in the routing tree at the time Iter is created. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.

Example
f, _ := New()
it := f.Iter()
for method, route := range it.All() {
	fmt.Println(method, route.Pattern())
}
Output:

func (Iter) Methods added in v0.16.0

func (it Iter) Methods() iter.Seq[string]

Methods returns a range iterator over all HTTP methods registered in the routing tree at the time Iter is created. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.

Example
f, _ := New()
it := f.Iter()
for method := range it.Methods() {
	fmt.Println(method)
}
Output:

func (Iter) Prefix added in v0.16.0

func (it Iter) Prefix(methods iter.Seq[string], prefix string) iter.Seq2[string, *Route]

Prefix returns a range iterator over all routes in the routing tree that match a given prefix and HTTP methods at the time Iter is created. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.

Example
f, _ := New()
it := f.Iter()
for method, route := range it.Prefix(slices.Values([]string{"GET", "POST"}), "/foo") {
	fmt.Println(method, route.Pattern())
}
Output:

func (Iter) Reverse added in v0.16.0

func (it Iter) Reverse(methods iter.Seq[string], host, path string) iter.Seq2[string, *Route]

Reverse returns a range iterator over all routes registered in the routing tree that match the given host and path for the provided HTTP methods. Unlike Iter.Routes, which matches an exact route, Reverse is used to match an url (e.g., a path from an incoming request) to a registered routes in the tree. The iterator reflect a snapshot of the routing tree at the time Iter is created.

If WithIgnoreTrailingSlash or WithRedirectTrailingSlash option is enabled on a route, Reverse will match it regardless of whether a trailing slash is present. If the path is empty, a default slash is automatically added.

This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.

Example
f, _ := New()
it := f.Iter()
for method, route := range it.Reverse(slices.Values([]string{"GET", "POST"}), "exemple.com", "/foo") {
	fmt.Println(method, route.Pattern())
}
Output:

func (Iter) Routes added in v0.16.0

func (it Iter) Routes(methods iter.Seq[string], pattern string) iter.Seq2[string, *Route]

Routes returns a range iterator over all registered routes in the routing tree that exactly match the provided route pattern for the given HTTP methods. The iterator reflect a snapshot of the routing tree at the time Iter is created. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.

Example
f, _ := New()
it := f.Iter()
for method, route := range it.Routes(slices.Values([]string{"GET", "POST"}), "/hello/{name}") {
	fmt.Println(method, route.Pattern())
}
Output:

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 Option added in v0.5.0

type Option interface {
	GlobalOption
	RouteOption
}

func WithClientIPResolver added in v0.19.0

func WithClientIPResolver(resolver ClientIPResolver) Option

WithClientIPResolver sets the resolver for obtaining the "real" client IP address from HTTP requests. This resolver is used by the [Context.ClientIP] method. The resolver 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 resolver is configured, [Context.ClientIP] returns ErrNoClientIPResolver.

This option can be applied on a per-route basis or globally:

  • If applied globally, it affects all routes by default.
  • If applied to a specific route, it will override the global setting for that route.
  • Setting the resolver to nil is equivalent to no resolver configured.

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.

This option can be applied on a per-route basis or globally:

  • If applied globally, it affects all routes by default.
  • If applied to a specific route, it will override the global setting for that route.

Note that this option is mutually exclusive with WithRedirectTrailingSlash, and if enabled will automatically deactivate WithRedirectTrailingSlash.

func WithMiddleware added in v0.7.0

func WithMiddleware(m ...MiddlewareFunc) Option

WithMiddleware attaches middleware to the router or to a specific route. The middlewares are executed in the order they are added. When applied globally, the middleware affects all handlers, including special handlers such as NotFound, MethodNotAllowed, AutoOption, and the internal redirect handler.

This option can be applied on a per-route basis or globally: - If applied globally, the middleware will be applied to all routes and handlers by default. - If applied to a specific route, the middleware will only apply to that route and will be chained after any global middleware.

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.Pattern(),
			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 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.

This option can be applied on a per-route basis or globally:

  • If applied globally, it affects all routes by default.
  • If applied to a specific route, it will override the global setting for that route.

Note that this option is mutually exclusive with WithIgnoreTrailingSlash, and if enabled will automatically deactivate WithIgnoreTrailingSlash.

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) 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
	// SetReadDeadline sets the deadline for reading the entire request, including the body. Reads from the request
	// body after the deadline has been exceeded will return an error. A zero value means no deadline. Setting the read
	// deadline after it has been exceeded will not extend it. If SetReadDeadline is not supported, it returns
	// an error matching [http.ErrNotSupported].
	SetReadDeadline(deadline time.Time) error
	// SetWriteDeadline sets the deadline for writing the response. Writes to the response body after the deadline has
	// been exceeded will not block, but may succeed if the data has been buffered. A zero value means no deadline.
	// Setting the write deadline after it has been exceeded will not extend it. If SetWriteDeadline is not supported,
	// it returns an error matching [http.ErrNotSupported].
	SetWriteDeadline(deadline time.Time) error
	// EnableFullDuplex indicates that the request handler will interleave reads from [http.Request.Body]
	// with writes to the [ResponseWriter].
	//
	// For HTTP/1 requests, the Go HTTP server by default consumes any unread portion of
	// the request body before beginning to write the response, preventing handlers from
	// concurrently reading from the request and writing the response.
	// Calling EnableFullDuplex disables this behavior and permits handlers to continue to read
	// from the request while concurrently writing the response.
	//
	// For HTTP/2 requests, the Go HTTP server always permits concurrent reads and responses.
	// If EnableFullDuplex is not supported, it returns an error matching [http.ErrNotSupported].
	EnableFullDuplex() error
}

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

type Route added in v0.16.0

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

Route represents an immutable HTTP route with associated handlers and settings.

func (*Route) Annotation added in v0.20.0

func (r *Route) Annotation(key any) any

Annotation returns the value associated with this Route for key, or nil if no value is associated with key. Successive calls to Annotation with the same key returns the same result.

func (*Route) ClientIPResolver added in v0.21.0

func (r *Route) ClientIPResolver() ClientIPResolver

ClientIPResolver returns the ClientIPResolver configured for the route, if any.

func (*Route) Handle added in v0.16.0

func (r *Route) Handle(c Context)

Handle calls the handler with the provided Context. See also Route.HandleMiddleware.

func (*Route) HandleMiddleware added in v0.17.0

func (r *Route) HandleMiddleware(c Context, _ ...struct{})

HandleMiddleware calls the handler with route-specific middleware applied, using the provided Context.

func (*Route) Hostname added in v0.18.0

func (r *Route) Hostname() string

Hostname returns the hostname part of the registered pattern if any.

func (*Route) IgnoreTrailingSlashEnabled added in v0.16.0

func (r *Route) IgnoreTrailingSlashEnabled() bool

IgnoreTrailingSlashEnabled returns whether the route is configured to ignore trailing slashes in requests when matching routes.

func (*Route) ParamsLen added in v0.21.0

func (r *Route) ParamsLen() int

ParamsLen returns the number of wildcard parameter for the route.

func (*Route) Path added in v0.16.0

func (r *Route) Path() string

Path returns the path part of the registered pattern.

func (*Route) Pattern added in v0.18.0

func (r *Route) Pattern() string

Pattern returns the registered route pattern.

func (*Route) RedirectTrailingSlashEnabled added in v0.16.0

func (r *Route) RedirectTrailingSlashEnabled() bool

RedirectTrailingSlashEnabled returns whether the route is configured to automatically redirect requests that include or omit a trailing slash.

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 RouteOption added in v0.18.0

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

func WithAnnotation added in v0.17.0

func WithAnnotation(key, value any) RouteOption

WithAnnotation attach arbitrary metadata to routes. Annotations are key-value pairs that allow middleware, handler or any other components to modify behavior based on the attached metadata. Unlike context-based metadata, which is tied to the request lifetime, annotations are bound to the route's lifetime and remain static across all requests for that route. The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages that use route annotation.

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 ...GlobalOption) (*Router, error)

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 route with the path "/hello/{name}", and set a simple handler that greets the
// user by their name.
r.MustHandle(http.MethodGet, "/hello/{name}", func(c Context) {
	_ = c.String(200, "Hello %s\n", c.Param("name"))
})

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

func (*Router) Delete

func (fox *Router) Delete(method, pattern string) (*Route, error)

Delete deletes an existing route for the given method and pattern. On success, it returns the deleted Route.

It's safe to delete a handler while the router is serving requests. This function is safe for concurrent use by multiple goroutine.

func (*Router) Handle added in v0.7.0

func (fox *Router) Handle(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

Handle registers a new route for the given method and pattern. On success, it returns the newly registered Route. If an error occurs, it returns one of the following:

It's safe to add a new handler while the router is serving requests. This function is safe for concurrent use by multiple goroutine. To override an existing handler, use Router.Update.

func (*Router) HandleRoute added in v0.21.0

func (fox *Router) HandleRoute(method string, route *Route) error

HandleRoute registers a new Route for the given method. If an error occurs, it returns one of the following:

It's safe to add a new route while the router is serving requests. This function is safe for concurrent use by multiple goroutine. To override an existing route, use Router.UpdateRoute.

func (*Router) Has added in v0.18.0

func (fox *Router) Has(method, pattern string) bool

Has allows to check if the given method and route pattern exactly match a registered route. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing. See also Router.Route as an alternative.

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)
exist := f.Has(http.MethodGet, "/hello/{name}")
fmt.Println(exist) // true
Output:

func (*Router) Iter added in v0.16.0

func (fox *Router) Iter() Iter

Iter returns a collection of range iterators for traversing registered methods and routes. It creates a point-in-time snapshot of the routing tree. Therefore, all iterators returned by Iter will not observe subsequent write on the router. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing.

func (*Router) Len added in v0.19.0

func (fox *Router) Len() int

Len returns the number of registered route.

func (*Router) Lookup

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

Lookup performs a manual route lookup for a given http.Request, returning the matched Route along with a ContextCloser, and a boolean indicating if the route was matched by adding or removing a trailing slash (trailing slash action recommended). If there is a direct match or a tsr is possible, Lookup always return a Route and a ContextCloser. The ContextCloser should always be closed if non-nil. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing. See also Router.Reverse as an alternative.

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.
		if cleanedPath == target {
			next(c)
			return
		}

		req.URL.Path = cleanedPath
		route, cc, tsr := c.Fox().Lookup(c.Writer(), req)
		if route != 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 || route.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 route.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) MustHandle added in v0.7.0

func (fox *Router) MustHandle(method, pattern string, handler HandlerFunc, opts ...RouteOption) *Route

MustHandle registers a new route for the given method and pattern. On success, it returns the newly registered Route. This function is a convenience wrapper for the Router.Handle function and panics on error.

func (*Router) NewRoute added in v0.21.0

func (fox *Router) NewRoute(pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

NewRoute create a new Route, configured with the provided options. If an error occurs, it returns one of the following:

func (*Router) Reverse added in v0.18.0

func (fox *Router) Reverse(method, host, path string) (route *Route, tsr bool)

Reverse perform a reverse lookup for the given method, host and path and return the matching registered Route (if any) along with a boolean indicating if the route was matched by adding or removing a trailing slash (trailing slash action recommended). If the path is empty, a default slash is automatically added. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing. See also Router.Lookup as an alternative.

Example

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

f, _ := New()
f.MustHandle(http.MethodGet, "exemple.com/hello/{name}", emptyHandler)
route, _ := f.Reverse(http.MethodGet, "exemple.com", "/hello/fox")
fmt.Println(route.Pattern()) // /hello/{name}
Output:

func (*Router) Route added in v0.18.0

func (fox *Router) Route(method, pattern string) *Route

Route performs a lookup for a registered route matching the given method and route pattern. It returns the Route if a match is found or nil otherwise. This function is safe for concurrent use by multiple goroutine and while mutation on route are ongoing. See also Router.Has as an alternative.

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) Stats added in v0.19.0

func (fox *Router) Stats() RouterInfo

Stats returns information on the configured global option.

func (*Router) Txn added in v0.18.0

func (fox *Router) Txn(write bool) *Txn

Txn create a new read-write or read-only transaction. Each Txn must be finalized with Txn.Commit or Txn.Abort. It's safe to create transaction from multiple goroutine and while the router is serving request. However, the returned Txn itself is NOT tread-safe. See also Router.Updates and Router.View for managed read-write and read-only transaction.

Example

This example demonstrate how to create an unmanaged read-write transaction.

f, _ := New()

// Txn create an unmanaged read-write or read-only transaction.
txn := f.Txn(true)
defer txn.Abort()

if _, err := txn.Handle(http.MethodGet, "exemple.com/hello/{name}", func(c Context) {
	_ = c.String(http.StatusOK, "hello %s", c.Param("name"))
}); 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 Prefix (or any other iterator).
for method, route := range it.Prefix(it.Methods(), "tmp.exemple.com/") {
	if _, err := f.Delete(method, route.Pattern()); err != nil {
		log.Printf("error deleting route: %s", err)
		return
	}
}
// Finalize the transaction
txn.Commit()
Output:

func (*Router) Update

func (fox *Router) Update(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

Update override an existing route for the given method and pattern. On success, it returns the newly registered Route. If an error occurs, it returns one of the following:

Route-specific option and middleware must be reapplied when updating a route. if not, any middleware and option will be removed, and the route will fall back to using global configuration (if any). It's safe to update a handler while the router is serving requests. This function is safe for concurrent use by multiple goroutine. To add new handler, use Router.Handle method.

func (*Router) UpdateRoute added in v0.21.0

func (fox *Router) UpdateRoute(method string, route *Route) error

UpdateRoute override an existing Route for the given method and new Route. If an error occurs, it returns one of the following:

It's safe to update a handler while the router is serving requests. This function is safe for concurrent use by multiple goroutine. To add new route, use Router.HandleRoute method.

func (*Router) Updates added in v0.18.0

func (fox *Router) Updates(fn func(txn *Txn) error) error

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. Updates returns any error returned by fn. This function is safe for concurrent use by multiple goroutine and while the router is serving request. However Txn itself is NOT tread-safe. See also Router.Txn for unmanaged transaction and Router.View for managed read-only transaction.

Example

This example demonstrate how to create a managed read-write transaction.

f, _ := New()

// 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 *Txn) error {
	if _, err := txn.Handle(http.MethodGet, "exemple.com/hello/{name}", func(c Context) {
		_ = c.String(http.StatusOK, "hello %s", c.Param("name"))
	}); 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 Prefix (or any other iterator).
	for method, route := range it.Prefix(it.Methods(), "tmp.exemple.com/") {
		if _, err := f.Delete(method, route.Pattern()); err != nil {
			return err
		}
	}
	return nil
}); err != nil {
	log.Printf("transaction aborted: %s", err)
}
Output:

func (*Router) View added in v0.18.0

func (fox *Router) View(fn func(txn *Txn) error) error

View executes a function within the context of a read-only managed transaction. View returns any error returned by fn. This function is safe for concurrent use by multiple goroutine and while mutation on routes are ongoing. However Txn itself is NOT tread-safe. See also Router.Txn for unmanaged transaction and Router.Updates for managed read-write transaction.

Example
f, _ := New()

// View executes a function within the context of a read-only managed transaction.
_ = f.View(func(txn *Txn) error {
	if txn.Has(http.MethodGet, "/foo") && txn.Has(http.MethodGet, "/bar") {
		// Do something
	}
	return nil
})
Output:

type RouterInfo added in v0.19.0

type RouterInfo struct {
	MaxRouteParams        uint16
	MaxRouteParamKeyBytes uint16
	MethodNotAllowed      bool
	AutoOptions           bool
	RedirectTrailingSlash bool
	IgnoreTrailingSlash   bool
	ClientIP              bool
}

RouterInfo hold information on the configured global options.

type TestContext added in v0.21.0

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

func NewTestContextOnly added in v0.7.0

func NewTestContextOnly(w http.ResponseWriter, r *http.Request, opts ...GlobalOption) *TestContext

NewTestContextOnly returns a new Context designed only for testing purpose.

func (TestContext) AddHeader added in v0.21.0

func (c TestContext) AddHeader(key, value string)

AddHeader add the response header for the given key to the specified value.

func (TestContext) Blob added in v0.21.0

func (c TestContext) Blob(code int, contentType string, buf []byte) (err error)

Blob sends a byte slice with the specified status code and content type.

func (TestContext) ClientIP added in v0.21.0

func (c TestContext) ClientIP() (*net.IPAddr, error)

ClientIP returns the "real" client IP address based on the configured ClientIPResolver. The resolver is set using the WithClientIPResolver option. If no resolver is configured, the method returns error ErrNoClientIPResolver.

The resolver used must be chosen and tuned for your network configuration. This should result in a resolver 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.

func (TestContext) Clone added in v0.21.0

func (c TestContext) Clone() Context

Clone returns a deep copy of the Context that is safe to use after the HandlerFunc returns. Any attempt to write on the ResponseWriter will panic with the error ErrDiscardedResponseWriter.

func (TestContext) CloneWith added in v0.21.0

func (c TestContext) CloneWith(w ResponseWriter, r *http.Request) ContextCloser

CloneWith returns a shallow 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.

func (TestContext) Close added in v0.21.0

func (c TestContext) Close()

Close releases the context to be reused later.

func (TestContext) Fox added in v0.21.0

func (c TestContext) Fox() *Router

Fox returns the Router instance.

func (TestContext) Header added in v0.21.0

func (c TestContext) Header(key string) string

Header retrieves the value of the request header for the given key.

func (TestContext) Host added in v0.21.0

func (c TestContext) Host() string

Host returns the request host.

func (TestContext) Method added in v0.22.0

func (c TestContext) Method() string

Method returns the request method.

func (TestContext) Param added in v0.21.0

func (c TestContext) Param(name string) string

Param retrieve a matching wildcard segment by name.

func (TestContext) Params added in v0.21.0

func (c TestContext) Params() iter.Seq[Param]

Params returns an iterator over the matched wildcard parameters for the current route.

func (TestContext) Path added in v0.21.0

func (c TestContext) Path() string

Path returns the request URL path.

func (TestContext) Pattern added in v0.21.0

func (c TestContext) Pattern() string

Pattern returns the registered route pattern or an empty string if the handler is called in a scope other than RouteHandler.

func (TestContext) QueryParam added in v0.21.0

func (c TestContext) QueryParam(name string) string

QueryParam returns the first value associated with the given key.

func (TestContext) QueryParams added in v0.21.0

func (c TestContext) QueryParams() url.Values

QueryParams parses the http.Request raw query and returns the corresponding values.

func (TestContext) Redirect added in v0.21.0

func (c TestContext) Redirect(code int, url string) error

Redirect sends an HTTP redirect response with the given status code and URL.

func (TestContext) RemoteIP added in v0.21.0

func (c TestContext) RemoteIP() *net.IPAddr

RemoteIP parses the IP from http.Request.RemoteAddr, normalizes it, and returns a net.IPAddr. It never returns nil, even if parsing the IP fails.

func (TestContext) Request added in v0.21.0

func (c TestContext) Request() *http.Request

Request returns the http.Request.

func (TestContext) Route added in v0.21.0

func (c TestContext) Route() *Route

Route returns the registered Route or nil if the handler is called in a scope other than RouteHandler.

func (TestContext) Scope added in v0.21.0

func (c TestContext) Scope() HandlerScope

Scope returns the HandlerScope associated with the current Context. This indicates the scope in which the handler is being executed, such as RouteHandler, NoRouteHandler, etc.

func (TestContext) SetHeader added in v0.21.0

func (c TestContext) SetHeader(key, value string)

SetHeader sets the response header for the given key to the specified value.

func (*TestContext) SetParams added in v0.21.0

func (c *TestContext) SetParams(params Params)

SetParams affect the provided params to this context.

func (TestContext) SetRequest added in v0.21.0

func (c TestContext) SetRequest(r *http.Request)

SetRequest sets the http.Request.

func (*TestContext) SetRoute added in v0.21.0

func (c *TestContext) SetRoute(route *Route)

SetRoute affect the provided route to this context. It also set the RouteHandler scope.

func (*TestContext) SetScope added in v0.21.0

func (c *TestContext) SetScope(scope HandlerScope)

SetScope affect the provided scope to this context.

func (TestContext) SetWriter added in v0.21.0

func (c TestContext) SetWriter(w ResponseWriter)

SetWriter sets the ResponseWriter.

func (TestContext) Stream added in v0.21.0

func (c TestContext) Stream(code int, contentType string, r io.Reader) (err error)

Stream sends data from an io.Reader with the specified status code and content type.

func (TestContext) String added in v0.21.0

func (c TestContext) String(code int, format string, values ...any) (err error)

String sends a formatted string with the specified status code.

func (TestContext) Writer added in v0.21.0

func (c TestContext) Writer() ResponseWriter

Writer returns the ResponseWriter.

type Txn added in v0.18.0

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

Txn is a read or write transaction on the routing tree.

func (*Txn) Abort added in v0.18.0

func (txn *Txn) Abort()

Abort cancel the transaction. This is a noop for read transactions, already aborted or committed transactions. This function is NOT thread-safe and should be run serially, along with all other Txn APIs.

func (*Txn) Commit added in v0.18.0

func (txn *Txn) Commit()

Commit finalize the transaction. This is a noop for read transactions, already aborted or committed transactions. This function is NOT thread-safe and should be run serially, along with all other Txn APIs.

func (*Txn) Delete added in v0.18.0

func (txn *Txn) Delete(method, pattern string) (*Route, error)

Delete deletes an existing route for the given method and pattern. On success, it returns the deleted Route. If an error occurs, it returns one of the following:

This function is NOT thread-safe and should be run serially, along with all other Txn APIs.

func (*Txn) Handle added in v0.18.0

func (txn *Txn) Handle(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

Handle registers a new route for the given method and pattern. On success, it returns the newly registered Route. If an error occurs, it returns one of the following:

This function is NOT thread-safe and should be run serially, along with all other Txn APIs. To override an existing handler, use Txn.Update.

func (*Txn) HandleRoute added in v0.21.0

func (txn *Txn) HandleRoute(method string, route *Route) error

HandleRoute registers a new Route for the given method. If an error occurs, it returns one of the following:

This function is NOT thread-safe and should be run serially, along with all other Txn APIs. To override an existing route, use Txn.UpdateRoute.

func (*Txn) Has added in v0.18.0

func (txn *Txn) Has(method, pattern string) bool

Has allows to check if the given method and route pattern exactly match a registered route. This function is NOT thread-safe and should be run serially, along with all other Txn APIs. See also Txn.Route as an alternative.

func (*Txn) Iter added in v0.18.0

func (txn *Txn) Iter() Iter

Iter returns a collection of range iterators for traversing registered routes. When called on a write transaction, Iter creates a point-in-time snapshot of the transaction state. Therefore, writing on the current transaction while iterating is allowed, but the mutation will not be observed in the result returned by iterators collection. This function is NOT thread-safe and should be run serially, along with all other Txn APIs.

func (*Txn) Len added in v0.19.0

func (txn *Txn) Len() int

Len returns the number of registered route.

func (*Txn) Lookup added in v0.18.0

func (txn *Txn) Lookup(w ResponseWriter, r *http.Request) (route *Route, cc ContextCloser, tsr bool)

Lookup performs a manual route lookup for a given http.Request, returning the matched Route along with a ContextCloser, and a boolean indicating if the route was matched by adding or removing a trailing slash (trailing slash action recommended). If there is a direct match or a tsr is possible, Lookup always return a Route and a ContextCloser. The ContextCloser should always be closed if non-nil. This function is NOT thread-safe and should be run serially, along with all other Txn APIs. See also Txn.Reverse as an alternative.

func (*Txn) Reverse added in v0.18.0

func (txn *Txn) Reverse(method, host, path string) (route *Route, tsr bool)

Reverse perform a reverse lookup for the given method, host and path and return the matching registered Route (if any) along with a boolean indicating if the route was matched by adding or removing a trailing slash (trailing slash action recommended). This function is NOT thread-safe and should be run serially, along with all other Txn APIs. See also Txn.Lookup as an alternative.

func (*Txn) Route added in v0.18.0

func (txn *Txn) Route(method, pattern string) *Route

Route performs a lookup for a registered route matching the given method and route pattern. It returns the Route if a match is found or nil otherwise. This function is NOT thread-safe and should be run serially, along with all other Txn APIs. See also Txn.Has as an alternative.

func (*Txn) Snapshot added in v0.18.0

func (txn *Txn) Snapshot() *Txn

Snapshot returns a point in time snapshot of the current state of the transaction. Returns a new read-only transaction or nil if the transaction is already aborted or commited.

func (*Txn) Truncate added in v0.18.0

func (txn *Txn) Truncate(methods ...string) error

Truncate remove all routes for the provided methods. If no methods are provided, all routes are truncated. Truncating on a read-only transaction returns ErrReadOnlyTxn.

func (*Txn) Update added in v0.18.0

func (txn *Txn) Update(method, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

Update override an existing route for the given method and pattern. On success, it returns the newly registered Route. If an error occurs, it returns one of the following:

Route-specific option and middleware must be reapplied when updating a route. if not, any middleware and option will be removed, and the route will fall back to using global configuration (if any). This function is NOT thread-safe and should be run serially, along with all other Txn APIs. To add a new handler, use Txn.Handle.

func (*Txn) UpdateRoute added in v0.21.0

func (txn *Txn) UpdateRoute(method string, route *Route) error

UpdateRoute override an existing Route for the given method and new Route. If an error occurs, it returns one of the following:

This function is NOT thread-safe and should be run serially, along with all other Txn APIs. To add a new route, use Txn.HandleRoute.

Directories

Path Synopsis
internal
constraints
Package constraints defines a set of useful constraints to be used with type parameters.
Package constraints defines a set of useful constraints to be used with type parameters.

Jump to

Keyboard shortcuts

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