fox

package module
v0.27.1 Latest Latest
Warning

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

Go to latest
Published: Feb 5, 2026 License: Apache-2.0 Imports: 36 Imported by: 3

README

Fox

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

[!NOTE] This repository has been transferred from github.com/tigerwill90/fox to github.com/fox-toolkit/fox. Existing users should update their imports and go.mod accordingly.

Fox is a lightweight and high performance HTTP request router for Go, designed for building reverse proxies, API gateways, or other applications that require managing routes at runtime based on configuration changes or external events. It is also well-suited for general use cases such as microservices and REST APIs, though it focuses on routing and does not include convenience helpers found in full-featured frameworks, such as automatic binding, content negotiation, file uploads, cookies, etc.

Fox supports mutation on its routing tree while handling requests concurrently. Internally, it uses a Radix Tree that supports lock-free reads while allowing a concurrent writer, and is optimized for high-concurrency reads and low-concurrency writes. The router supports complex routing patterns, enforces clear priority rules, and performs strict validation to prevent misconfigurations.

Disclaimer

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

Features

Runtime updates: Register, update and delete route handler safely at any time without impact on performance.

Flexible routing: Fox strikes a balance between routing flexibility, performance and clarity by enforcing clear priority rules, ensuring that there are no unintended matches and maintaining high performance even for complex routing patterns. Supported features include named parameters, suffix and infix catch-all, regexp constraints, hostname matching, method and method-less routes, route matchers, and sub-routers.

Trailing slash handling: Automatically handle trailing slash inconsistencies by either ignoring them, redirecting to the canonical path, or enforcing strict matching based on your needs.

Path correction: Automatically handle malformed paths with extra slashes or dots by either serving the cleaned path directly or redirecting to the canonical form.

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

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

Growing middleware ecosystem: Fox's middleware ecosystem is still limited, but standard http.Handler middleware are fully compatible. Contributions are welcome!



Getting started

Install

With a correctly configured Go toolchain:

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

import (
	"errors"
	"fmt"
	"log"
	"net/http"

	"github.com/fox-toolkit/fox"
)

func HelloServer(c *fox.Context) {
	_ = c.String(http.StatusOK, fmt.Sprintf("Hello %s\n", c.Param("name")))
}

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

	f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/hello/{name}", HelloServer)

	if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatalln(err)
	}
}
Named parameters

Routes can include named parameters using curly braces {name} to match exactly one non-empty route segment. The matching segments are recorded as Param and accessible via the Context. Named parameters are supported anywhere in the route, but only one parameter is allowed per segment (or hostname label) and must appear at the end of the segment.

Pattern /avengers/{name}

/avengers/ironman               matches
/avengers/thor                  matches
/avengers/hulk/angry            no matches
/avengers/                      no matches

Pattern /users/uuid:{id}

/users/uuid:123                 matches
/users/uuid:                    no matches

Pattern /users/uuid:{id}/config

/users/uuid:123/config          matches
/users/uuid:/config             no matches

Pattern {sub}.example.com/avengers

first.example.com/avengers      matches
example.com/avengers           no matches

Named parameters can include regular expression using the syntax {name:regexp}. Regular expressions cannot contain capturing groups, but can use non-capturing groups (?:pattern) instead. Regexp support is opt-in via fox.AllowRegexpParam(true) option.

Pattern /products/{name:[A-Za-z]+}

/products/laptop        matches
/products/123           no matches
Named Wildcards (Catch-all)

Named wildcard start with a plus sign + followed by a name {param} and match any sequence of characters including slashes, but cannot match an empty string. The matching segments are also accessible via Context. Catch-all parameters are supported anywhere in the route, but only one parameter is allowed per segment (or hostname label) and must appear at the end of the segment. Consecutive catch-all parameter are not allowed.

Pattern /src/+{filepath}

/src/conf.txt                      matches
/src/dir/config.txt                 matches
/src/                              no matches

Pattern /src/file=+{path}

/src/file=config.txt                 matches
/src/file=/dir/config.txt            matches
/src/file=                          no matches

Pattern: /assets/+{path}/thumbnail

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

Pattern +{sub}.example.com/avengers

first.example.com/avengers          matches
first.second.example.com/avengers   matches
example.com/avengers               no matches

Optional named wildcard start with an asterisk * followed by a name {param} and match any sequence of characters including empty strings. Unlike +{param}, optional wildcards can only be used as a suffix.

Pattern /src/*{filepath}

/src/conf.txt                      matches
/src/dir/config.txt                 matches
/src/                              matches

Pattern /src/file=*{path}

/src/file=config.txt                 matches
/src/file=/dir/config.txt            matches
/src/file=                          matches

Named wildcard can include regular expression using the syntax +{name:regexp} or *{name:regexp}. Regular expressions cannot contain capturing groups, but can use non-capturing groups (?:pattern) instead. Regexp support is opt-in via fox.AllowRegexpParam(true) option.

Pattern /src/+{filepath:[A-Za-z/]+\.json}

/src/dir/config.json            matches
/src/dir/config.txt             no matches
Route matchers

Route matchers enable routing decisions based on request properties beyond methods, hostname and path. Multiple routes can share the same pattern and methods and be differentiated by query parameters, headers, client IP, or custom criteria.

f.MustAdd(fox.MethodGet, "/api/users", V1Handler, fox.WithHeaderMatcher("X-API-Version", "v1"))
f.MustAdd(fox.MethodGet, "/api/users", V2Handler, fox.WithHeaderMatcher("X-API-Version", "v2"))
f.MustAdd(fox.MethodGet, "/api/users", V1Handler) // Fallback route

Built-in matchers include fox.WithQueryMatcher, fox.WithQueryRegexpMatcher, fox.WithHeaderMatcher, fox.WithHeaderRegexpMatcher, and fox.WithClientIPMatcher. Multiple matchers on a route use AND logic. Routes without matchers serve as fallbacks. For custom matching logic, implement the fox.Matcher interface and use fox.WithMatcher. See Priority rules for matcher evaluation order.

Method-less routes

Routes can be registered without specifying an HTTP method to match any method. The constant fox.MethodAny is a convenience placeholder equivalent to an empty method set (nil or empty slice).

// Handle any method on /health
f.MustAdd(fox.MethodAny, "/health", HealthHandler)
// Forward all requests to a backend service
f.MustAdd(fox.MethodAny, "/api/*{any}", ProxyHandler)

Routes registered with a specific HTTP method always take precedence over method-less routes. This allows defining method-specific behavior while falling back to a generic handler for other methods.

// Specific handler for GET requests
f.MustAdd(fox.MethodGet, "/resource", GetHandler)
// All other methods handled here
f.MustAdd(fox.MethodAny, "/resource", FallbackHandler)
Sub-Routers

Fox provides a composable routing API where routers can be mounted as regular routes, each with its own middleware and configuration.

api := fox.MustRouter(fox.WithMiddleware(AuthMiddleware()))
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/", HelloHandler)
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users", ListUser)
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users/{id}", GetUser)
api.MustAdd(fox.MethodPost, "/users", CreateUser)

f := fox.MustRouter(fox.DefaultOptions())
f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/*{filepath}", fox.WrapH(http.FileServer(http.Dir("./public/"))))
f.MustAdd(fox.MethodAny, "/api*{mount}", fox.Sub(api))

Requests matching the prefix are delegated to the mounted router with the remaining path.

Use cases include:

  • Applying middleware, matchers or other configuration to a route prefix
  • Managing entire route subtree at runtime (e.g. insert, update, or delete via the parent router)
  • Organizing routes into groups with shared configuration
Hostname validation & restrictions

Hostnames are validated to conform to the LDH (letters, digits, hyphens) rule (lowercase only) and SRV-like "underscore labels". Wildcard segments within hostnames, such as {sub}.example.com/, are exempt from LDH validation since they act as placeholders rather than actual domain labels. As such, they do not count toward the hard limit of 63 characters per label, nor the 253-character limit for the full hostname. Internationalized domain names (IDNs) should be specified using an ASCII (Punycode) representation.

Priority rules

The router is designed to balance routing flexibility, performance, and predictability. Internally, it uses a radix tree to store routes efficiently. When a request arrives, Fox evaluates routes in the following order:

  1. Hostname matching

    • Routes with hostnames are evaluated before path-only routes
  2. Pattern matching (longest match, most specific first)

    • Static segments
    • Named parameters with regex constraints
    • Named parameters without constraints
    • Catch-all parameters with regex constraints
    • Catch-all parameters without constraints
    • Infix catch-all are evaluated before suffix catch-all (e.g., /bucket/+{path}/meta before /bucket/+{path})
    • At the same level, multiple regex-constrained parameters are evaluated in registration order
  3. Method matching

    • Routes with specific methods are evaluated before method-less routes
  4. Matcher evaluation (for routes sharing the same pattern and overlapping methods)

    • Routes with matchers are evaluated before routes without
    • Among routes with matchers, higher priority is evaluated first (configurable via fox.WithMatcherPriority, or defaults to the number of matchers)
    • Routes with equal priority may be evaluated in any order

If a match candidate fails to complete the full route, including matchers, Fox returns to the last decision point and tries the next available alternative following the same priority order.

Hostname routing

The router can transition instantly and transparently from path-only mode to hostname-prioritized mode without any additional configuration or action. If any route with a hostname is registered, the router automatically switches to prioritize hostname matching. Conversely, if no hostname-specific routes are registered, the router reverts to path-priority mode.

  • If the router has no routes registered with hostnames, the router will perform a path-based lookup only.
  • If the router includes at least one route with a hostname, the router will prioritize lookup based on the request host and path. If no match is found, the router will then fall back to a path-only lookup.

Hostname matching is case-insensitive, so requests to Example.COM, example.com, and EXAMPLE.COM will all match a route registered for example.com.

Warning about context

The fox.Context instance is freed once the request handler function returns to optimize resource allocation. If you need to retain fox.Context beyond the scope of the handler, use the fox.Context.Clone methods.

func Hello(c *fox.Context) {
    cc := c.Clone()
    go func() {
        time.Sleep(2 * time.Second)
        log.Println(cc.Param("name")) // Safe
    }()
    _ = c.String(http.StatusOK, "Hello %s\n", c.Param("name"))
}

Concurrency

Fox implements an immutable radix tree with copy-on-write semantics, which support lock-free reads while allowing a single concurrent writer. Mutations follow a three-phase pattern: first, descend recursively through the tree to locate the insertion point; then as the call stack unwinds, copy each node along the modified path back to the root and finally, update the root in a single atomic operation. The result is a shallow copy of the tree, where unmodified branches are shared between the old and new tree. Multiple mutations can be applied in a single transaction, where each cloned node is cached to avoid copying it more than once.

Other key points
  • Routing requests is lock-free (reading thread never block, even while writes are ongoing)
  • The router always sees a consistent version of the tree while routing request
  • Reading threads do not block writing threads (adding, updating or removing a handler can be done concurrently)
  • Writing threads block each other but never block reading threads

As such threads that route requests should never encounter latency due to ongoing writes or other concurrent readers.

Managing routes at runtime
Routing mutation

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

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/fox-toolkit/fox"
)

type Data struct {
	Pattern string   `json:"pattern"`
	Methods []string `json:"methods"`
	Text    string   `json:"text"`
}

func Action(c *fox.Context) {
	data := new(Data)
	if err := json.NewDecoder(c.Request().Body).Decode(data); err != nil {
		http.Error(c.Writer(), err.Error(), http.StatusBadRequest)
		return
	}

	var err error
	action := c.Param("action")
	switch action {
	case "add":
		_, err = c.Router().Add(data.Methods, data.Pattern, func(c *fox.Context) {
			_ = c.String(http.StatusOK, data.Text)
		})
	case "update":
		_, err = c.Router().Update(data.Methods, data.Pattern, func(c *fox.Context) {
			_ = c.String(http.StatusOK, data.Text)
		})
	case "delete":
		_, err = c.Router().Delete(data.Methods, data.Pattern)
	default:
		http.Error(c.Writer(), fmt.Sprintf("action %q is not allowed", action), http.StatusBadRequest)
		return
	}
	if err != nil {
		http.Error(c.Writer(), err.Error(), http.StatusConflict)
		return
	}

	_ = c.String(http.StatusOK, fmt.Sprintf("%s route [%s] %s: success\n", action, strings.Join(data.Methods, ","), data.Pattern))
}

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

	f.MustAdd(fox.MethodPost, "/routes/{action}", Action)

	if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatalln(err)
	}
}
ACID Transaction

Fox supports read-write and read-only transactions (with Atomicity, Consistency, and Isolation; Durability is not supported as transactions are in memory). Thread that route requests always see a consistent version of the routing tree and are fully isolated from an ongoing transaction until committed. Read-only transactions capture a point-in-time snapshot of the tree, ensuring they do not observe any ongoing or committed changes made after their creation.

Managed read-write transaction
// Updates executes a function within the context of a read-write managed transaction. If no error is returned
// from the function then the transaction is committed. If an error is returned then the entire transaction is
// aborted.
if err := f.Updates(func(txn *fox.Txn) error {
	if _, err := txn.Add(fox.MethodGet, "example.com/hello/{name}", Handler); err != nil {
		return err
	}

	// Iter returns a collection of range iterators for traversing registered routes.
	it := txn.Iter()
	// When Iter() is called on a write transaction, it creates a point-in-time snapshot of the transaction state.
	// It means that writing on the current transaction while iterating is allowed, but the mutation will not be
	// observed in the result returned by PatternPrefix (or any other iterator).
	for route := range it.PatternPrefix("tmp.example.com/") {
		if _, err := txn.Delete(slices.Collect(route.Methods()), route.Pattern()); err != nil {
			return err
		}
	}
	return nil
}); err != nil {
	log.Printf("transaction aborted: %s", err)
}
Managed read-only transaction
_ = f.View(func(txn *fox.Txn) error {
	if txn.Has(fox.MethodGet, "/foo") {
		if txn.Has(fox.MethodGet, "/bar") {
			// do something
		}
	}
	return nil
})
Unmanaged read-write transaction
// Txn create an unmanaged read-write or read-only transaction.
txn := f.Txn(true)
defer txn.Abort()

if _, err := txn.Add(fox.MethodGet, "example.com/hello/{name}", Handler); err != nil {
	log.Printf("error inserting route: %s", err)
	return
}

// Iter returns a collection of range iterators for traversing registered routes.
it := txn.Iter()
// When Iter() is called on a write transaction, it creates a point-in-time snapshot of the transaction state.
// It means that writing on the current transaction while iterating is allowed, but the mutation will not be
// observed in the result returned by PatternPrefix (or any other iterator).
for route := range it.PatternPrefix("tmp.example.com/") {
	if _, err := txn.Delete(slices.Collect(route.Methods()), route.Pattern()); err != nil {
		log.Printf("error deleting route: %s", err)
		return
	}
}
// Finalize the transaction
txn.Commit()

Middleware

Middlewares can be registered globally using the fox.WithMiddleware option. The example below demonstrates how to create and apply automatically a simple logging middleware to all routes (including 404, 405, etc...).

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/fox-toolkit/fox"
)

func Logger(next fox.HandlerFunc) fox.HandlerFunc {
	return func(c *fox.Context) {
		start := time.Now()
		next(c)
		log.Printf("route: %s, latency: %s, status: %d, size: %d",
			c.Pattern(),
			time.Since(start),
			c.Writer().Status(),
			c.Writer().Size(),
		)
	}
}

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

	f.MustAdd(fox.MethodGet, "/", func(c *fox.Context) {
		_ = c.String(http.StatusOK, "Hello World")
	})

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

Additionally, fox.WithMiddlewareFor option provide a more fine-grained control over where a middleware is applied, such as only for 404 or 405 handlers. Possible scopes include fox.RouteHandlers (regular routes), fox.NoRouteHandler, fox.NoMethodHandler, fox.RedirectSlashHandler, fox.RedirectPathHandler, fox.OptionsHandler and any combination of these.

f  := fox.MustRouter(
	fox.WithMiddlewareFor(fox.RouteHandler, Logger),
	fox.WithMiddlewareFor(fox.NoRouteHandler|fox.NoMethodHandler, SpecialLogger),
)

Finally, it's also possible to attaches middleware on a per-route basis. Note that route-specific middleware must be explicitly reapplied when updating a route. If not, any middleware will be removed, and the route will fall back to using only global middleware (if any).

f := fox.MustRouter(
	fox.WithMiddleware(fox.Logger(slog.NewTextHandler(os.Stdout, nil))),
)
f.MustAdd(fox.MethodGet, "/", SomeHandler, fox.WithMiddleware(foxtimeout.Middleware(2*time.Second)))
f.MustAdd(fox.MethodGet, "/foo", SomeOtherHandler)
Official middlewares

Working with http.Handler

Fox itself implements the http.Handler interface which make easy to chain any compatible middleware before the router. Moreover, the router provides convenient fox.WrapF, fox.WrapH and fox.WrapM adapter to be use with http.Handler.

The route parameters can be accessed by the wrapped handler through the request context.Context when the adapters are used.

Wrapping an http.Handler

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

f := fox.MustRouter()
f.MustAdd(fox.MethodGet, "/articles/{id}", fox.WrapH(articles))

Wrapping any standard http.Handler middleware

corsMw, _ := cors.NewMiddleware(cors.Config{
	Origins:        []string{"https://example.com"},
	Methods:        []string{http.MethodGet, http.MethodPost, http.MethodPut},
	RequestHeaders: []string{"Authorization"},
})

f := fox.MustRouter(
	fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
)

Handling OPTIONS Requests and CORS Automatically

The WithAutoOptions setting or the WithOptionsHandler registration enable automatic responses to OPTIONS requests. This feature is particularly useful for handling Cross-Origin Resource Sharing (CORS) preflight requests.

When automatic OPTIONS responses is enabled, Fox distinguishes between regular OPTIONS requests and CORS preflight requests:

  • Regular OPTIONS requests: The router responds with the Allow header populated with all HTTP methods registered for the matched resource. If no route matches, the NoRoute handler is called.
  • CORS preflight requests: The router responds to every preflight request by calling the OPTIONS handler, regardless of whether the resource exists.

To customize how OPTIONS requests are handled (e.g. adding CORS headers), you may register a middleware for the fox.OptionsHandler scope or provide a custom handler via WithOptionsHandler. Note that registered routes with the OPTIONS method always take precedence over automatic replies.

package main

import (
	"errors"
	"log"
	"net/http"

	"github.com/jub0bs/cors"
	"github.com/fox-toolkit/fox"
)

func main() {
	corsMw, err := cors.NewMiddleware(cors.Config{
		Origins:        []string{"https://example.com"},
		Methods:        []string{http.MethodGet, http.MethodPost},
		RequestHeaders: []string{"Authorization"},
	})
	if err != nil {
		log.Fatal(err)
	}
	corsMw.SetDebug(true) // turn debug mode on (optional)

	f := fox.MustRouter(
		fox.WithAutoOptions(true), // let Fox automatically handle OPTIONS requests
		fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
	)

	f.MustAdd(fox.MethodGet, "/api/users", ListUsers)
	f.MustAdd(fox.MethodPost, "/api/users", CreateUsers)

	if err := http.ListenAndServe(":8080", f); !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}

Alternatively, you can use a sub-router to apply CORS only to a specific section of your API.

package main

import (
	"log"
	"net/http"

	"github.com/jub0bs/cors"
	"github.com/fox-toolkit/fox"
)

func main() {
	corsMw, err := cors.NewMiddleware(cors.Config{
		Origins:        []string{"https://example.com"},
		Methods:        []string{http.MethodHead, http.MethodGet, http.MethodPost},
		RequestHeaders: []string{"Authorization"},
	})
	if err != nil {
		log.Fatal(err)
	}
	corsMw.SetDebug(true) // turn debug mode on (optional)

	f := fox.MustRouter()
	f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/*{filepath}", fox.WrapH(http.FileServer(http.Dir("./public/"))))

	api := fox.MustRouter(
		fox.WithAutoOptions(true), // let Fox automatically handle OPTIONS requests
		fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
	)
	api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users", ListUsers)
	api.MustAdd(fox.MethodPost, "/users", CreateUser)

	f.MustAdd(fox.MethodAny, "/api*{any}", fox.Sub(api)) // note: Method-less route
}

The CORS protocol is complex and security-sensitive. We do NOT recommend implementing CORS handling manually. Instead, consider using jub0bs/cors, which performs extensive validation before allowing middleware creation, helping you avoid common pitfalls.

Resolving Client IP

The WithClientIPResolver option allows you to set up strategies to resolve the client IP address based on your use case and network topology. Accurately determining the client IP is hard, particularly in environments with proxies or load balancers. For example, the leftmost IP in the X-Forwarded-For header is commonly used and is often regarded as the "closest to the client" and "most real," but it can be easily spoofed. Therefore, you should absolutely avoid using it for any security-related purposes, such as request throttling.

The resolver used must be chosen and tuned for your network configuration. This should result in a resolver never returning an error and if it does, it should be treated as an application issue or a misconfiguration, rather than defaulting to an untrustworthy IP.

The sub-package github.com/fox-toolkit/fox/clientip provides a set of best practices resolvers that should cover most use cases.

package main

import (
	"fmt"

	"github.com/fox-toolkit/fox"
	"github.com/fox-toolkit/fox/clientip"
)

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

	f.MustAdd(fox.MethodGet, "/foo/bar", func(c *fox.Context) {
		ipAddr, err := c.ClientIP()
		if err != nil {
			// If the current resolver is not able to derive the client IP, an error
			// will be returned rather than falling back on an untrustworthy IP. It
			// should be treated as an application issue or a misconfiguration.
			panic(err)
		}
		fmt.Println(ipAddr.String())
	})
}

It is also possible to create a chain with multiple resolvers that attempt to derive the client IP, stopping when the first one succeeds.

resolver, _ := clientip.NewLeftmostNonPrivate(clientip.ForwardedKey, 10)
f := fox.MustRouter(
	fox.DefaultOptions(),
	fox.WithClientIPResolver(
		// A common use for this is if a server is both directly connected to the
		// internet and expecting a header to check.
		clientip.NewChain(
			resolver,
			clientip.NewRemoteAddr(),
		),
	),
)

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

See this blog post 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:   Darwin
GOARCH: arm64
GO:     1.25
CPU:    Apple M4 Max
Static Routes

It is just a collection of random static paths inspired by the structure of the Go directory. It might not be a realistic URL-structure.

GOMAXPROCS: 1

BenchmarkHttpRouter_StaticAll     300577              3961 ns/op               0 B/op          0 allocs/op
BenchmarkHttpTreeMux_StaticAll    231432              5131 ns/op               0 B/op          0 allocs/op
BenchmarkGin_StaticAll            220321              5461 ns/op               0 B/op          0 allocs/op
BenchmarkEcho_StaticAll           180883              6529 ns/op               0 B/op          0 allocs/op
BenchmarkFox_StaticAll            156045              7468 ns/op               0 B/op          0 allocs/op
BenchmarkStdMux_StaticAll          67579             17499 ns/op               0 B/op          0 allocs/op
BenchmarkChi_StaticAll             33232             35855 ns/op           57776 B/op        314 allocs/op
BenchmarkBeego_StaticAll           19587             61820 ns/op           55264 B/op        471 allocs/op
BenchmarkGorillaMux_StaticAll       4173            285869 ns/op          133137 B/op       1099 allocs/op
BenchmarkMartini_StaticAll          2148            557924 ns/op          129210 B/op       2031 allocs/op
BenchmarkTraffic_StaticAll          1894            615963 ns/op          749842 B/op      14444 allocs/op
BenchmarkPat_StaticAll              1600            735826 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

BenchmarkFox_StaticAllParallel-16                1946833               604.9 ns/op             0 B/op          0 allocs/op
BenchmarkHttpTreeMux_StaticAllParallel-16          48470               24786 ns/op             0 B/op          0 allocs/op

As you can see, this benchmark highlight the cost of using higher synchronisation primitive like RWMutex to be able to register new route while handling requests.

Micro Benchmarks

The following benchmarks measure the cost of some very basic operations.

In the first benchmark, only a single route, containing a parameter, is loaded into the routers. Then a request for a URL matching this pattern is made and the router has to call the respective registered handler function. End.

GOMAXPROCS: 1

BenchmarkEcho_Param             68284310                17.85 ns/op            0 B/op          0 allocs/op
BenchmarkGin_Param              54732352                22.31 ns/op            0 B/op          0 allocs/op
BenchmarkFox_Param              52690413                22.69 ns/op            0 B/op          0 allocs/op
BenchmarkHttpRouter_Param       39168165                30.12 ns/op           32 B/op          1 allocs/op
BenchmarkHttpTreeMux_Param       7755874                152.9 ns/op          352 B/op          3 allocs/op
BenchmarkChi_Param               3487819                318.2 ns/op          704 B/op          4 allocs/op
BenchmarkBeego_Param             3648824                331.0 ns/op          352 B/op          3 allocs/op
BenchmarkPat_Param               2904813                427.8 ns/op          552 B/op         12 allocs/op
BenchmarkGorillaMux_Param        1838157                578.3 ns/op         1152 B/op          8 allocs/op
BenchmarkTraffic_Param           1000000                 1060 ns/op         1888 B/op         23 allocs/op
BenchmarkMartini_Param            994087                 1256 ns/op         1096 B/op         12 allocs/op

Same as before, but now with multiple parameters, all in the same single route. The intention is to see how the routers scale with the number of parameters.

GOMAXPROCS: 1

BenchmarkGin_Param5             28328834                41.64 ns/op            0 B/op          0 allocs/op
BenchmarkEcho_Param5            27899163                41.74 ns/op            0 B/op          0 allocs/op
BenchmarkFox_Param5             21644923                55.66 ns/op            0 B/op          0 allocs/op
BenchmarkHttpRouter_Param5      14973546                78.72 ns/op          160 B/op          1 allocs/op
BenchmarkHttpTreeMux_Param5      3642981                331.3 ns/op          576 B/op          6 allocs/op
BenchmarkChi_Param5              2879529                414.7 ns/op          704 B/op          4 allocs/op
BenchmarkBeego_Param5            2779600                430.0 ns/op          352 B/op          3 allocs/op
BenchmarkGorillaMux_Param5       1327518                908.9 ns/op         1216 B/op          8 allocs/op
BenchmarkPat_Param5              1233646                960.1 ns/op         1008 B/op         29 allocs/op
BenchmarkMartini_Param5           839385                 1418 ns/op         1256 B/op         13 allocs/op
BenchmarkTraffic_Param5           729421                 1623 ns/op         2400 B/op         32 allocs/op

BenchmarkEcho_Param20            9627675               126.1 ns/op             0 B/op          0 allocs/op
BenchmarkGin_Param20             9537547               126.3 ns/op             0 B/op          0 allocs/op
BenchmarkFox_Param20             5420937               217.4 ns/op             0 B/op          0 allocs/op
BenchmarkHttpRouter_Param20      4762604               253.2 ns/op           704 B/op          1 allocs/op
BenchmarkBeego_Param20           1000000                1060 ns/op           352 B/op          3 allocs/op
BenchmarkChi_Param20              696405                1639 ns/op          2504 B/op          9 allocs/op
BenchmarkHttpTreeMux_Param20      696096                1715 ns/op          3144 B/op         13 allocs/op
BenchmarkGorillaMux_Param20       579686                2014 ns/op          3272 B/op         13 allocs/op
BenchmarkMartini_Param20          418554                2750 ns/op          3568 B/op         18 allocs/op
BenchmarkPat_Param20              288222                4100 ns/op          4752 B/op         83 allocs/op
BenchmarkTraffic_Param20          213721                5529 ns/op          8416 B/op         60 allocs/op

Now let's see how expensive it is to access a parameter. The handler function reads the value (by the name of the parameter, e.g. with a map lookup; depends on the router) and writes it to /dev/null

GOMAXPROCS: 1

BenchmarkGin_ParamWrite                 40427803                27.51 ns/op            0 B/op          0 allocs/op
BenchmarkFox_ParamWrite                 40477408                29.27 ns/op            0 B/op          0 allocs/op
BenchmarkHttpRouter_ParamWrite          34413740                34.31 ns/op           32 B/op          1 allocs/op
BenchmarkEcho_ParamWrite                24441432                46.73 ns/op            8 B/op          1 allocs/op
BenchmarkHttpTreeMux_ParamWrite          7640900                157.9 ns/op          352 B/op          3 allocs/op
BenchmarkChi_ParamWrite                  3914731                313.0 ns/op          704 B/op          4 allocs/op
BenchmarkBeego_ParamWrite                3504256                335.4 ns/op          360 B/op          4 allocs/op
BenchmarkGorillaMux_ParamWrite           2001657                579.7 ns/op         1152 B/op          8 allocs/op
BenchmarkPat_ParamWrite                  1923120                632.2 ns/op          976 B/op         16 allocs/op
BenchmarkTraffic_ParamWrite               981200                 1199 ns/op         2312 B/op         27 allocs/op
BenchmarkMartini_ParamWrite               895435                 1354 ns/op         1144 B/op         15 allocs/op

In those micro benchmarks, we can see that Fox scale really well, even with long wildcard routes. Like Gin, this router reuse the data structure (e.g. fox.Context slice) containing the matching parameters in order to remove completely heap allocation.

Github

Finally, this benchmark executes a request for each GitHub API route (203 routes).

GOMAXPROCS: 1

BenchmarkGin_GithubAll            129974              9004 ns/op               0 B/op          0 allocs/op
BenchmarkEcho_GithubAll           103280             11493 ns/op               0 B/op          0 allocs/op
BenchmarkFox_GithubAll             86886             13653 ns/op               0 B/op          0 allocs/op
BenchmarkHttpRouter_GithubAll      82833             13830 ns/op           14240 B/op        171 allocs/op
BenchmarkHttpTreeMux_GithubAll     26180             44966 ns/op           67648 B/op        691 allocs/op
BenchmarkChi_GithubAll             14157             83664 ns/op          130817 B/op        740 allocs/op
BenchmarkBeego_GithubAll           12570             95787 ns/op           73121 B/op        629 allocs/op
BenchmarkTraffic_GithubAll          1018           1170450 ns/op          855242 B/op      15102 allocs/op
BenchmarkGorillaMux_GithubAll        886           1338005 ns/op          230339 B/op       1620 allocs/op
BenchmarkMartini_GithubAll           709           1519244 ns/op          236945 B/op       2805 allocs/op
BenchmarkPat_GithubAll               636           1721062 ns/op         1853068 B/op      29560 allocs/op

Road to v1

Contributions

This project aims to provide a lightweight, high-performance router that is easy to use and hard to misuse, designed for building API gateways and reverse proxies. Features are chosen carefully with an emphasis on composability, and each addition is evaluated against this core mission. The router exposes a relatively low-level API, allowing it to serve as a building block for implementing your own "batteries included" frameworks. Feature requests and PRs along these lines are welcome.

License

Fox is licensed under the Apache License 2.0. See LICENSE.txt for details.

The Fox logo is licensed separately under CC BY-NC-ND 4.0. See LICENSE-fox-logo.txt for details.

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

View Source
const (
	// LoggerStatusKey is the key used by the built-in logger middleware for the HTTP response status code
	// when the log method is called. The associated [slog.Value] is a string.
	LoggerStatusKey = "status"
	// LoggerMethodKey is the key used by the built-in logger middleware for the HTTP request method.
	// The associated [slog.Value] is a string.
	LoggerMethodKey = "method"
	// LoggerHostKey is the key used by the built-in logger middleware for the request host.
	// The associated [slog.Value] is a string.
	LoggerHostKey = "host"
	// LoggerPathKey is the key used by the built-in logger middleware for the request path.
	// The associated [slog.Value] is a string.
	LoggerPathKey = "path"
	// LoggerLatencyKey is the key used by the built-in logger middleware for the request processing duration.
	// The associated [slog.Value] is a time.Duration.
	LoggerLatencyKey = "latency"
	// LoggerSizeKey is the key used by the built-in logger middleware for the response body size.
	// The associated [slog.Value] is an int.
	LoggerSizeKey = "size"
	// LoggerLocationKey is the key used by the built-in logger middleware for redirect location header.
	// The associated [slog.Value] is a string.
	LoggerLocationKey = "location"
)

Keys for "built-in" logger attribute for the logger middleware.

View Source
const (
	// LoggerRouteKey is the key used by the built-in recovery middleware for the matched route
	// when the log method is called. The associated [slog.Value] is a string.
	LoggerRouteKey = "route"
	// LoggerParamsKey is the key used by the built-in recovery middleware for route parameters
	// when the log method is called. The associated [slog.Value] is a [slog.GroupValue] containing parameter
	// key-value pairs.
	LoggerParamsKey = "params"
	// LoggerPanicKey is the key used by the built-in recovery middleware for the panic value
	// when the log method is called. The associated [slog.Value] is any.
	LoggerPanicKey = "panic"
)

Keys for "built-in" logger attribute for the recovery middleware. Keys for "built-in" logger attributes used by the recovery middleware.

Variables

View Source
var (
	ErrRouteNotFound           = errors.New("route not found")
	ErrRouteConflict           = errors.New("route conflict")
	ErrRouteNameExist          = errors.New("route name already registered")
	ErrInvalidRoute            = errors.New("invalid route")
	ErrDiscardedResponseWriter = errors.New("discarded response writer")
	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")
	ErrTooManyMatchers         = errors.New("too many matchers")
	ErrRegexpNotAllowed        = errors.New("regexp not allowed")
	ErrInvalidConfig           = errors.New("invalid config")
	ErrInvalidMatcher          = errors.New("invalid matcher")
)
View Source
var (
	MethodGet     = []string{http.MethodGet}
	MethodHead    = []string{http.MethodHead}
	MethodPost    = []string{http.MethodPost}
	MethodPut     = []string{http.MethodPut}
	MethodPatch   = []string{http.MethodPatch}
	MethodDelete  = []string{http.MethodDelete}
	MethodConnect = []string{http.MethodConnect}
	MethodOptions = []string{http.MethodOptions}
	MethodTrace   = []string{http.MethodTrace}
)

Common HTTP methods.

View Source
var MethodAny []string

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

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

func DefaultMethodNotAllowedHandler(c *Context)

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

func DefaultNotFoundHandler

func DefaultNotFoundHandler(c *Context)

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

func DefaultOptionsHandler

func DefaultOptionsHandler(c *Context)

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

func ErrNotSupported

func ErrNotSupported() error

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

func NewTestContext

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

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

func WithClientIPMatcher

func WithClientIPMatcher(ip string) interface {
	RouteOption
	MatcherOption
}

WithClientIPMatcher attaches a client IP address matcher to a route. The matcher ensures that requests are only routed to the handler if the client IP address matches the specified CIDR notation or IP address. The ip parameter accepts both single IP addresses (e.g., "192.168.1.1") and CIDR ranges (e.g., "192.168.1.0/24"). Multiple matchers can be attached to the same route. All matchers must match for the route to be eligible. See WithClientIPResolver to configure a resolver for obtaining the "real" client IP.

func WithClientIPResolver

func WithClientIPResolver(resolver ClientIPResolver) interface {
	GlobalOption
	RouteOption
}

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 WithHandleTrailingSlash

func WithHandleTrailingSlash(opt TrailingSlashOption) interface {
	GlobalOption
	RouteOption
}

WithHandleTrailingSlash configures how the router handles trailing slashes in request paths.

Available slash handling modes:

  • StrictSlash: Routes are matched exactly as registered. /foo/bar and /foo/bar/ are treated as different routes.
  • RelaxedSlash: Routes match regardless of trailing slash. Both /foo/bar and /foo/bar/ match the same route.
  • RedirectSlash: When a route is not found, but exists with/without a trailing slash, issues a redirect to the correct path.

Redirects use URL.RawPath if set, otherwise URL.Path.

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.

If both /foo/bar and /foo/bar/ are explicitly registered, the exact match always takes precedence. The trailing slash handling logic only applies when there is no direct match but a match would be possible by adding or removing a trailing slash.

func WithHeaderMatcher

func WithHeaderMatcher(key, value string) interface {
	RouteOption
	MatcherOption
}

WithHeaderMatcher attaches an HTTP header matcher to a route. The matcher ensures that requests are only routed to the handler if the specified header matches the given value. Multiple matchers can be attached to the same route. All matchers must match for the route to be eligible.

func WithHeaderRegexpMatcher

func WithHeaderRegexpMatcher(key, expr string) interface {
	RouteOption
	MatcherOption
}

WithHeaderRegexpMatcher attaches an HTTP header matcher with regular expression support to a route. The matcher ensures that requests are only routed to the handler if the specified header value matches the given regular expression. The expression is automatically anchored at both ends, requiring a full match of the header value. Multiple matchers can be attached to the same route. All matchers must match for the route to be eligible.

func WithMatcher

func WithMatcher(matchers ...Matcher) interface {
	RouteOption
	MatcherOption
}

WithMatcher attaches a custom matcher to a route. Matchers allow for advanced request routing based on conditions beyond the request host, path and method. Multiple matchers can be attached to the same route. All matchers must match for the route to be eligible.

func WithMiddleware

func WithMiddleware(m ...MiddlewareFunc) interface {
	GlobalOption
	RouteOption
}

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, _ := NewRouter(WithMiddleware(metrics))

f.MustAdd([]string{http.MethodGet, http.MethodHead}, "/hello/{name}", func(c *Context) {
	_ = c.String(200, fmt.Sprintf("Hello %s\n", c.Param("name")))
})

func WithQueryMatcher

func WithQueryMatcher(key, value string) interface {
	RouteOption
	MatcherOption
}

WithQueryMatcher attaches a query parameter matcher to a route. The matcher ensures that requests are only routed to the handler if the specified query parameter matches the given value. Multiple matchers can be attached to the same route. All matchers must match for the route to be eligible.

func WithQueryRegexpMatcher

func WithQueryRegexpMatcher(key, expr string) interface {
	RouteOption
	MatcherOption
}

WithQueryRegexpMatcher attaches a query parameter matcher with regular expression support to a route. The matcher ensures that requests are only routed to the handler if the specified query parameter value matches the given regular expression. The expression is automatically anchored at both ends, requiring a full match of the parameter value. Multiple matchers can be attached to the same route. All matchers must match for the route to be eligible.

Types

type ClientIPResolver

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 RequestContext) (*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/fox-toolkit/fox/clientip package.

type ClientIPResolverFunc

type ClientIPResolverFunc func(c RequestContext) (*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

ClientIP calls f(c).

type ClientIpMatcher

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

func MatchClientIP

func MatchClientIP(ip string) (ClientIpMatcher, error)

func (ClientIpMatcher) Equal

func (m ClientIpMatcher) Equal(matcher Matcher) bool

func (ClientIpMatcher) IPNet

func (m ClientIpMatcher) IPNet() *net.IPNet

func (ClientIpMatcher) Match

func (m ClientIpMatcher) Match(c RequestContext) bool

func (ClientIpMatcher) String

func (m ClientIpMatcher) String() string

type Context

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

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 Context may be reused as soon as the handler returns.

func NewTestContextOnly

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

NewTestContextOnly returns a new Context designed only for testing purpose.

func (*Context) AddHeader

func (c *Context) AddHeader(key, value string)

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

func (*Context) Blob

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

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

func (*Context) ClientIP

func (c *Context) 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 (*Context) Clone

func (c *Context) 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 (*Context) CloneWith

func (c *Context) CloneWith(w ResponseWriter, r *http.Request) *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 caller is responsible for closing the returned Context by calling Context.Close when it is 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 (*Context) Close

func (c *Context) Close()

Close releases the context to be reused later. This method must be called for contexts obtained via Context.CloneWith, Router.Lookup, or Txn.Lookup. Contexts passed to a HandlerFunc are managed automatically by the router and should not be closed manually. See also Context for more details.

func (*Context) Header

func (c *Context) Header(key string) string

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

func (*Context) Host

func (c *Context) Host() string

Host returns the request host.

func (*Context) Method

func (c *Context) Method() string

Method returns the request method.

func (*Context) Param

func (c *Context) Param(name string) string

Param retrieve a matching wildcard segment by name.

func (*Context) Params

func (c *Context) Params() iter.Seq[Param]

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

func (*Context) Path

func (c *Context) Path() string

Path returns the request url.URL.RawPath if not empty, or fallback to the url.URL.Path.

func (*Context) Pattern

func (c *Context) Pattern() string

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

func (*Context) QueryParam

func (c *Context) QueryParam(name string) string

QueryParam returns the first query value associated with the given key. The query parameters are parsed and cached on first access.

func (*Context) QueryParams

func (c *Context) QueryParams() url.Values

QueryParams parses the http.Request raw query and returns the corresponding values. The result is cached after the first call.

func (*Context) RemoteIP

func (c *Context) 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 (*Context) Request

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

Request returns the http.Request.

func (*Context) Route

func (c *Context) Route() *Route

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

func (*Context) Router

func (c *Context) Router() *Router

Router returns the Router instance.

func (*Context) Scope

func (c *Context) 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 (*Context) SetHeader

func (c *Context) SetHeader(key, value string)

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

func (*Context) SetRequest

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

SetRequest sets the http.Request.

func (*Context) SetWriter

func (c *Context) SetWriter(w ResponseWriter)

SetWriter sets the ResponseWriter.

func (*Context) Stream

func (c *Context) 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 (*Context) String

func (c *Context) String(code int, s string) (err error)

String sends a formatted string with the specified status code.

func (*Context) Writer

func (c *Context) Writer() ResponseWriter

Writer returns the ResponseWriter.

type FixedPathOption

type FixedPathOption uint8
const (
	StrictPath FixedPathOption = iota
	RelaxedPath
	RedirectPath
)

type GlobalOption

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

func AllowRegexpParam

func AllowRegexpParam(enable bool) GlobalOption

AllowRegexpParam enables support for regular expressions in route parameters. When enabled, parameters can include regex patterns (e.g., {id:[0-9]+}). When disabled, routes containing regex patterns will fail with and error that Is ErrInvalidRoute and ErrRegexpNotAllowed.

func DefaultOptions

func DefaultOptions() GlobalOption

DefaultOptions configures the router with sensible production defaults:

For development, consider combining this with WithPrettyLogs to add debugging middleware.

func WithAutoOptions

func WithAutoOptions(enable bool) GlobalOption

WithAutoOptions enables automatic responses to OPTIONS requests with, by default, a 204 status code. For regular OPTIONS requests, the router responds with the "Allow" header listing methods registered for the matched routes, or calls the NoRoute handler if no route matches. For CORS preflight requests, the router always responds to all request by calling the OPTIONS handler. 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 WithHandleFixedPath

func WithHandleFixedPath(opt FixedPathOption) GlobalOption

WithHandleFixedPath configures how the router handles non-canonical request paths containing extraneous elements like double slashes, dots, or parent directory references.

Available path handling modes:

  • StrictPath: No path cleaning is performed. Routes are matched only as requested (disables this feature).
  • RelaxedPath: After normal lookup fails, tries matching with a cleaned path. If found, serves the handler directly.
  • RedirectPath: After normal lookup fails, tries matching with a cleaned path. If found, redirects to the clean path.

Redirects use URL.RawPath if set, otherwise URL.Path.

This option applies globally to all routes and cannot be configured per-route. See CleanPath for details on how paths are cleaned.

func WithMaxRouteMatchers

func WithMaxRouteMatchers(max int) GlobalOption

WithMaxRouteMatchers set the maximum number of matchers allowed in a route. The default max is math.MaxUint8. Routes exceeding this limit will fail with an error that Is ErrInvalidRoute and ErrTooManyMatchers.

func WithMaxRouteParamKeyBytes

func WithMaxRouteParamKeyBytes(max int) GlobalOption

WithMaxRouteParamKeyBytes set the maximum number of bytes allowed per parameter key in a route. The default max is math.MaxUint8. Routes with parameter keys exceeding this limit will fail with an error that Is ErrInvalidRoute and ErrParamKeyTooLarge.

func WithMaxRouteParams

func WithMaxRouteParams(max int) GlobalOption

WithMaxRouteParams set the maximum number of parameters allowed in a route. The default max is math.MaxUint8. Routes exceeding this limit will fail with an error that is ErrInvalidRoute and ErrTooManyParams.

func WithMiddlewareFor

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, RedirectSlashHandler, RedirectPathHandler, OptionsHandler, and any combination of these. Use this option when you need fine-grained control over where the middleware is applied.

func WithNoMethod

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

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

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

func WithOptionsHandler(handler HandlerFunc) GlobalOption

WithOptionsHandler register an HandlerFunc which is called on automatic OPTIONS requests. By default, the router respond with a 204 status code. The "Allow" header it automatically set before calling the handler (except for CORS preflight request). Note that custom OPTIONS handler take priority over automatic replies. By default, DefaultOptionsHandler is used. Note that this option automatically enable WithAutoOptions.

func WithPrettyLogs

func WithPrettyLogs() GlobalOption

WithPrettyLogs configures the router with human-readable, colorized logging optimized for terminal output. It registers the following middleware at the front of the chain:

This option prioritizes readability over performance and is not recommended for high-throughput applications. For production workloads, prefer structured logging with a performance-oriented slog.Handler such as zerolog or zap.

func WithSystemWideOptions

func WithSystemWideOptions(enable bool) GlobalOption

WithSystemWideOptions enable automatic response for system-wide OPTIONS request (OPTIONS *). When this option is enabled, the router responds with a 200 OK status code and the "Allow" header listing all HTTP methods used across registered routes. Note that to let Fox handle system-wide OPTIONS requests, http.Server.DisableGeneralOptionsHandler must be set to true. This option is enabled by default.

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 Sub

func Sub(router *Router) HandlerFunc

Sub returns a HandlerFunc that mounts the provided Router as a sub-router. Requests matching the parent route prefix are delegated to the sub-router which handles the remaining path. The parent route pattern should end with a catch-all. Parameters captured by the parent route are preserved and accessible alongside any parameters matched by the sub-router. Similarly, http.Request.Pattern is the concatenation of the parent and sub-router patterns. See also Router.Add for registering the handler.

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

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
	// RedirectSlashHandler scope applies to the internal redirect trailing slash handler, used for handling requests with trailing slashes.
	RedirectSlashHandler
	// RedirectPathHandler scope applies to the internal redirect fixed path handler, used for handling requests that need path cleaning.
	RedirectPathHandler
	// 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 | RedirectSlashHandler | RedirectPathHandler | OptionsHandler
)

type HeaderMatcher

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

func MatchHeader

func MatchHeader(key, value string) (HeaderMatcher, error)

func (HeaderMatcher) Equal

func (m HeaderMatcher) Equal(matcher Matcher) bool

func (HeaderMatcher) Key

func (m HeaderMatcher) Key() string

func (HeaderMatcher) Match

func (m HeaderMatcher) Match(c RequestContext) bool

func (HeaderMatcher) String

func (m HeaderMatcher) String() string

func (HeaderMatcher) Value

func (m HeaderMatcher) Value() string

type HeaderRegexpMatcher

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

func MatchHeaderRegexp

func MatchHeaderRegexp(key, expr string) (HeaderRegexpMatcher, error)

func (HeaderRegexpMatcher) Equal

func (m HeaderRegexpMatcher) Equal(matcher Matcher) bool

func (HeaderRegexpMatcher) Key

func (m HeaderRegexpMatcher) Key() string

func (HeaderRegexpMatcher) Match

func (HeaderRegexpMatcher) Regex

func (m HeaderRegexpMatcher) Regex() *regexp.Regexp

func (HeaderRegexpMatcher) String

func (m HeaderRegexpMatcher) String() string

type Iter

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

func (it Iter) All() iter.Seq[*Route]

All returns a range iterator over all routes registered in the routing tree. 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. See also Iter.PatternPrefix as an alternative.

Example
f, _ := NewRouter()
it := f.Iter()
for route := range it.All() {
	fmt.Println(slices.Collect(route.Methods()), route.Pattern())
}

func (Iter) Methods

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

Methods returns a range iterator over all HTTP methods registered in the routing tree. 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, _ := NewRouter()
it := f.Iter()
for method := range it.Methods() {
	fmt.Println(method)
}

func (Iter) NamePrefix

func (it Iter) NamePrefix(prefix string) iter.Seq[*Route]

NamePrefix returns a range iterator over all routes in the routing tree that match a given name prefix. 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, _ := NewRouter()
it := f.Iter()
for route := range it.NamePrefix("ns:default/") {
	fmt.Println(slices.Collect(route.Methods()), route.Name())
}

func (Iter) Names

func (it Iter) Names() iter.Seq[*Route]

Names returns a range iterator over all routes registered in the routing tree with a name. 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. See also Iter.NamePrefix as an alternative.

Example
f, _ := NewRouter()
it := f.Iter()
for route := range it.Names() {
	fmt.Println(slices.Collect(route.Methods()), route.Pattern())
}

func (Iter) PatternPrefix

func (it Iter) PatternPrefix(prefix string) iter.Seq[*Route]

PatternPrefix returns a range iterator over all routes in the routing tree that match a given pattern prefix. 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. Note: Partial parameter syntax (e.g., /users/{name:) is not supported and will not match any routes.

Example
f, _ := NewRouter()
it := f.Iter()
for route := range it.PatternPrefix("/v1/") {
	fmt.Println(slices.Collect(route.Methods()), route.Pattern())
}

func (Iter) Routes

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

Routes returns a range iterator over all registered routes in the routing tree that exactly match the provided route pattern. 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, _ := NewRouter()
it := f.Iter()
for route := range it.Routes("/hello/{name}") {
	fmt.Println(slices.Collect(route.Methods()), route.Pattern())
}

type Matcher

type Matcher interface {
	// Match evaluates if the [RequestContext] satisfies this matcher.
	Match(c RequestContext) bool
	// Equal reports whether this matcher is semantically equivalent to another. Implementation must
	// - Handle type checking: matchers of different types are not equal
	// - Be reflexive: m.Equal(m) == true
	// - Be symmetric: m.Equal(n) == n.Equal(m)
	Equal(m Matcher) bool
}

Matcher evaluates if an HTTP request satisfies specific conditions. Matchers are evaluated after hostname and path matching succeeds. All matchers associated with a route must match for the route to be selected. Matcher implementations must be safe for concurrent use by multiple goroutines.

type MatcherOption

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

type MiddlewareFunc

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 Logger

func Logger(handler slog.Handler) MiddlewareFunc

Logger 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. Status codes are logged at different levels: 2xx at INFO, 3xx at DEBUG (with Location header if present), 4xx at WARN, and 5xx at ERROR.

func Recovery

func Recovery(handler slog.Handler) MiddlewareFunc

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

func RecoveryWithFunc

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

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

func WrapM

func WrapM(m func(http.Handler) http.Handler) MiddlewareFunc

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

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

func (p Params) Has(name string) bool

Has checks whether the parameter exists by name.

type QueryMatcher

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

func MatchQuery

func MatchQuery(key, value string) (QueryMatcher, error)

func (QueryMatcher) Equal

func (m QueryMatcher) Equal(matcher Matcher) bool

func (QueryMatcher) Key

func (m QueryMatcher) Key() string

func (QueryMatcher) Match

func (m QueryMatcher) Match(c RequestContext) bool

func (QueryMatcher) String

func (m QueryMatcher) String() string

func (QueryMatcher) Value

func (m QueryMatcher) Value() string

type QueryRegexpMatcher

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

func MatchQueryRegexp

func MatchQueryRegexp(key, expr string) (QueryRegexpMatcher, error)

func (QueryRegexpMatcher) Equal

func (m QueryRegexpMatcher) Equal(matcher Matcher) bool

func (QueryRegexpMatcher) Key

func (m QueryRegexpMatcher) Key() string

func (QueryRegexpMatcher) Match

func (QueryRegexpMatcher) Regex

func (m QueryRegexpMatcher) Regex() *regexp.Regexp

func (QueryRegexpMatcher) String

func (m QueryRegexpMatcher) String() string

type RecoveryFunc

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 RequestContext

type RequestContext interface {
	// Request returns the current [http.Request].
	Request() *http.Request
	// 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)
	// Method returns the request method.
	Method() string
	// Path returns the request [url.URL.RawPath] if not empty, or fallback to the [url.URL.Path].
	Path() string
	// Host returns the request host.
	Host() string
	// QueryParams parses the [http.Request] raw query and returns the corresponding values. The result is cached after
	// the first call.
	QueryParams() url.Values
	// QueryParam returns the first query value associated with the given key. The query parameters are parsed and
	// cached on first access.
	QueryParam(name string) string
	// Header retrieves the value of the request header for the given key.
	Header(key string) string
	// Pattern returns the registered route pattern or an empty string if the handler is called in a scope other than [RouteHandler].
	Pattern() string
}

RequestContext provides read-only access to incoming HTTP request data, including request properties, headers, query parameters, and client IP information. It is implemented by Context.

The RequestContext API is not thread-safe. Its lifetime is limited to the scope of its caller: within a Matcher, it is valid only for the duration of the [Matcher.Match] call; within a HandlerFunc, it is valid only for the duration of the handler execution. The underlying context may be reused after the call returns.

type ResponseWriter

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

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

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

func (*Route) Annotation

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

func (r *Route) ClientIPResolver() ClientIPResolver

ClientIPResolver returns the ClientIPResolver configured for this Route, if any.

func (*Route) Handle

func (r *Route) Handle(c *Context)

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

func (*Route) HandleMiddleware

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

HandleMiddleware calls the handler with route-specific middleware applied, using the provided Context. This method is not intended to be used as the handler for another route, as the middleware chain would be duplicated when registered. To reuse a route's handler, use Route.Handle directly or register a new route via Router.AddRoute or Router.UpdateRoute.

func (*Route) Hostname

func (r *Route) Hostname() string

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

func (*Route) Matchers

func (r *Route) Matchers() iter.Seq[Matcher]

Matchers returns an iterator over all matchers attached to this Route.

func (*Route) MatchersLen

func (r *Route) MatchersLen() int

MatchersLen returns the number of matchers for this Route.

func (*Route) MatchersPriority

func (r *Route) MatchersPriority() uint

MatchersPriority returns the matchers priority for this Route.

func (*Route) Methods

func (r *Route) Methods() iter.Seq[string]

Methods returns an iterator over all HTTP methods this route responds to (if any), in lexicographical order.

func (*Route) Name

func (r *Route) Name() string

Name returns the name of this Route.

func (*Route) Params

func (r *Route) Params() iter.Seq[string]

Params returns an iterator over all parameters name for this Route.

func (*Route) ParamsLen

func (r *Route) ParamsLen() int

ParamsLen returns the number of parameters for this Route.

func (*Route) Path

func (r *Route) Path() string

Path returns the path part of the registered pattern.

func (*Route) Pattern

func (r *Route) Pattern() string

Pattern returns the registered route pattern.

func (*Route) String

func (r *Route) String() string

func (*Route) TrailingSlashOption

func (r *Route) TrailingSlashOption() TrailingSlashOption

TrailingSlashOption returns the configured TrailingSlashOption for this Route.

type RouteConflictError

type RouteConflictError struct {
	// New is the route that was being registered when the conflict was detected.
	New *Route
	// Conflicts contains the previously registered routes that conflict with New.
	Conflicts []*Route
	// contains filtered or unexported fields
}

RouteConflictError represents a conflict that occurred during route registration. It contains the route being registered, and the existing routes that caused the conflict.

func (*RouteConflictError) Error

func (e *RouteConflictError) Error() string

func (*RouteConflictError) Unwrap

func (e *RouteConflictError) Unwrap() error

Unwrap returns the sentinel value ErrRouteConflict.

type RouteMatch

type RouteMatch struct {
	*Route
	// Tsr is true when the match required trailing slash adjustment.
	Tsr bool
}

RouteMatch represents a route matched by a reverse lookup operation.

type RouteNameConflictError

type RouteNameConflictError struct {
	// New is the route that was being registered when the conflict was detected.
	New *Route
	// Conflict is the previously registered route that conflict with New.
	Conflict *Route
}

RouteNameConflictError represents a conflict that occurred during route name registration. It contains the route being registered, and the existing route that caused the conflict.

func (*RouteNameConflictError) Error

func (e *RouteNameConflictError) Error() string

func (*RouteNameConflictError) Unwrap

func (e *RouteNameConflictError) Unwrap() error

Unwrap returns the sentinel value ErrRouteNameExist.

type RouteOption

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

func WithAnnotation

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. See also [WithAnnotationFunc]

func WithMatcherPriority

func WithMatcherPriority(priority uint) RouteOption

WithMatcherPriority sets the priority for a route with matchers. When multiple routes share the same pattern (regardless of param names) and have overlapping methods, matchers are evaluated by priority (highest first). Routes with equal priority may be evaluated in any order. Routes without matchers are always evaluated last. If unset or 0, the priority defaults to the number of matchers. Note that routes with specific methods are always evaluated before method-less routes, regardless of priority.

func WithName

func WithName(name string) RouteOption

WithName assigns a name to a route for identification and lookup purposes. The name must be unique among all other routes registered.

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 MustRouter

func MustRouter(opts ...GlobalOption) *Router

MustRouter returns a ready to use instance of Fox router. This function is a convenience wrapper for NewRouter and panics on error.

func NewRouter

func NewRouter(opts ...GlobalOption) (*Router, error)

NewRouter returns a ready to use instance of Fox router.

func (*Router) Add

func (fox *Router) Add(methods []string, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

Add registers a new route for the given methods, pattern and matchers. 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) AddRoute

func (fox *Router) AddRoute(route *Route) error

AddRoute registers a new Route. 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) Delete

func (fox *Router) Delete(methods []string, pattern string, opts ...MatcherOption) (*Route, error)

Delete deletes an existing route for the given methods, pattern and matchers. 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) DeleteRoute

func (fox *Router) DeleteRoute(route *Route) (*Route, error)

DeleteRoute deletes an existing route that match the provided Route pattern and matchers. On success, it returns the deleted Route. If an error occurs, it returns one of the following:

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

func (fox *Router) HandleNoRoute(c *Context)

HandleNoRoute calls the no route handler with the provided Context. Note that this bypasses any middleware attached to the no route handler.

func (*Router) Has

func (fox *Router) Has(methods []string, pattern string, matchers ...Matcher) bool

Has allows to check if the given methods, pattern and matchers 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
f, _ := NewRouter()
f.MustAdd([]string{http.MethodGet, http.MethodHead}, "/hello/{name}", emptyHandler)
exist := f.Has([]string{http.MethodGet, http.MethodHead}, "/hello/{name}")
fmt.Println(exist) // true

func (*Router) Iter

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

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 *Context, tsr bool)

Lookup performs a manual route lookup for a given http.Request, returning the matched Route along with a Context, 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 Context. The Context 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.Match as an alternative.

func (*Router) Match

func (fox *Router) Match(method string, r *http.Request) (route *Route, tsr bool)

Match perform a reverse lookup for the given method and http.Request. It returns 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 safe for concurrent use by multiple goroutine and while mutation on routes are ongoing. See also Router.Lookup as an alternative.

Example
f, _ := NewRouter()
f.MustAdd([]string{http.MethodGet, http.MethodHead}, "exemple.com/hello/{name}", emptyHandler)

req := httptest.NewRequest(http.MethodGet, "/hello/fox", nil)

route, tsr := f.Match(req.Method, req)
fmt.Println(route.Pattern(), tsr) // exemple.com/hello/{name} false

func (*Router) MustAdd

func (fox *Router) MustAdd(methods []string, pattern string, handler HandlerFunc, opts ...RouteOption) *Route

MustAdd registers a new route for the given methods, pattern and matchers. On success, it returns the newly registered Route. This function is a convenience wrapper for the Router.Add function and panics on error.

func (*Router) Name

func (fox *Router) Name(name string) *Route

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

func (*Router) NewRoute

func (fox *Router) NewRoute(methods []string, 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) Route

func (fox *Router) Route(methods []string, pattern string, matchers ...Matcher) *Route

Route performs a lookup for a registered route matching the given methods, pattern and matchers. 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 or Iter.Routes as an alternative.

func (*Router) RouterInfo

func (fox *Router) RouterInfo() RouterInfo

RouterInfo returns information on the configured global option.

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

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, _ := NewRouter()

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

if _, err := txn.Add([]string{http.MethodGet, http.MethodHead}, "exemple.com/hello/{name}", func(c *Context) {
	_ = c.String(http.StatusOK, fmt.Sprintf("Hello %s\n", 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 PatternPrefix (or any other iterator).
for route := range it.PatternPrefix("tmp.exemple.com/") {
	if _, err := f.Delete(slices.Collect(route.Methods()), route.Pattern()); err != nil {
		log.Printf("error deleting route: %s", err)
		return
	}
}
// Finalize the transaction
txn.Commit()

func (*Router) Update

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

Update override an existing route for the given methods, pattern and matchers. 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 (or reset to their default value), 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.Add method.

func (*Router) UpdateRoute

func (fox *Router) UpdateRoute(route *Route) error

UpdateRoute override an existing Route for the given 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.AddRoute method.

func (*Router) Updates

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, _ := NewRouter()

// 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.Add([]string{http.MethodGet, http.MethodHead}, "exemple.com/hello/{name}", func(c *Context) {
		_ = c.String(http.StatusOK, fmt.Sprintf("Hello %s\n", 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 PatternPrefix (or any other iterator).
	for route := range it.PatternPrefix("tmp.exemple.com/") {
		if _, err := f.Delete(slices.Collect(route.Methods()), route.Pattern()); err != nil {
			return err
		}
	}
	return nil
}); err != nil {
	log.Printf("transaction aborted: %s", err)
}

func (*Router) View

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

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

f, _ := NewRouter()

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

type RouterInfo

type RouterInfo struct {
	MaxRouteParams        int
	MaxRouteParamKeyBytes int
	MaxRouteMatchers      int
	TrailingSlashOption   TrailingSlashOption
	FixedPathOption       FixedPathOption
	MethodNotAllowed      bool
	AutoOptions           bool
	SystemWideOptions     bool
	ClientIP              bool
	AllowRegexp           bool
}

RouterInfo hold information on the configured global options.

type TrailingSlashOption

type TrailingSlashOption uint8
const (
	StrictSlash TrailingSlashOption = iota
	RelaxedSlash
	RedirectSlash
)

type Txn

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

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

func (*Txn) Abort

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

func (txn *Txn) Add(methods []string, pattern string, handler HandlerFunc, opts ...RouteOption) (*Route, error)

Add registers a new route for the given methods, pattern and matchers. 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) AddRoute

func (txn *Txn) AddRoute(route *Route) error

AddRoute registers a 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 override an existing route, use Txn.UpdateRoute.

func (*Txn) Commit

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

func (txn *Txn) Delete(methods []string, pattern string, opts ...MatcherOption) (*Route, error)

Delete deletes an existing route for the given methods, pattern and matchers. 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) DeleteRoute

func (txn *Txn) DeleteRoute(route *Route) (*Route, error)

DeleteRoute deletes an existing route that match the provided Route pattern and matchers. 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) Has

func (txn *Txn) Has(methods []string, pattern string, matchers ...Matcher) bool

Has allows to check if the given methods, pattern and matchers 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

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

func (txn *Txn) Len() int

Len returns the number of registered route.

func (*Txn) Lookup

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

Lookup performs a manual route lookup for a given http.Request, returning the matched Route along with a Context, 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 Context. The Context 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.Match as an alternative.

func (*Txn) Match

func (txn *Txn) Match(method string, r *http.Request) (route *Route, tsr bool)

Match perform a reverse lookup for the given method and http.Request. It returns 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) Name

func (txn *Txn) Name(name string) *Route

Name performs a lookup for a registered route matching the given method and route name. 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.Route as an alternative.

func (*Txn) Route

func (txn *Txn) Route(methods []string, pattern string, matchers ...Matcher) *Route

Route performs a lookup for a registered route matching the given methods, pattern and matchers. 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 or Iter.Routes as an alternative.

func (*Txn) Snapshot

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

func (txn *Txn) Truncate() error

Truncate remove all routes registered in the router. Truncating on a read-only transaction returns ErrReadOnlyTxn.

func (*Txn) Update

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

Update override an existing route for the given methods, pattern and matchers. 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 (or reset to their default value), 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.Add.

func (*Txn) UpdateRoute

func (txn *Txn) UpdateRoute(route *Route) error

UpdateRoute override an existing Route for the given 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.AddRoute.

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