Documentation
¶
Overview ¶
Package sim provides a small, idiomatic HTTP router built on top of net/http.ServeMux, extending it with method-based routing helpers such as App.Get, App.Post, and App.Any.
Example:
package main
import (
"context"
"log/slog"
"net/http"
"github.com/qm012/sim"
)
func main() {
app := sim.Default()
app.Get("/", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("root."))
})
app.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user " + r.PathValue("id")))
})
app.Group("/api", func(r sim.Router) {
r.Post("/users", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
})
})
if err := app.Run(context.Background(), ":3333"); err != nil {
slog.Error("server failed", "err", err)
}
}
Routes are registered on an App, which implements http.Handler and can be passed directly to http.ListenAndServe or served with App.Run, which shuts the server down gracefully when its context is canceled.
Patterns ¶
Pattern matching uses the same syntax and precedence rules as http.ServeMux since Go 1.22. A pattern may carry an optional method and host prefix, and a path may contain wildcard segments such as {name} and {name...}. Wildcard values are read from the request with http.Request.PathValue. For example:
- "GET /users/{id}" matches only GET requests, capturing the id.
- "/static/" matches every method and any path under "/static/".
- "/files/{path...}" matches the remainder of the URL, including slashes.
The method helpers register the same pattern for a single method: App.Get registers "GET /path", App.Post registers "POST /path", and App.Any registers "/path" for every method. The pattern given to a method helper must be a plain path; method prefixes belong to the helper itself.
See the http.ServeMux documentation for the complete pattern syntax, precedence rules, and trailing-slash redirection behavior.
Wrappers registered with App.Use are applied to every handler registered after the call, with the first wrapper outermost. Chain composes wrappers into one; ChainFunc is its counterpart over http.HandlerFunc, the type accepted by the method helpers such as App.Get. Default returns an App with the standard wrappers already registered.
See the documentation of App for the full routing API.
Index ¶
- Variables
- func Chain(ss ...func(http.Handler) http.Handler) func(http.Handler) http.Handler
- func ChainFunc(ss ...func(http.Handler) http.Handler) func(http.HandlerFunc) http.HandlerFunc
- func ClientIPFromContext(ctx context.Context) string
- type App
- func (a *App) Any(path string, handlerFunc http.HandlerFunc)
- func (a *App) Connect(path string, handlerFunc http.HandlerFunc)
- func (a *App) Delete(path string, handlerFunc http.HandlerFunc)
- func (a *App) Get(path string, handlerFunc http.HandlerFunc)
- func (a *App) Group(relativePath string, fn func(r Router))
- func (a *App) Handle(pattern string, handler http.Handler)
- func (a *App) HandleFunc(pattern string, handlerFunc http.HandlerFunc)
- func (a *App) Handler(r *http.Request) (http.Handler, string)
- func (a *App) Head(path string, handlerFunc http.HandlerFunc)
- func (a *App) Options(path string, handlerFunc http.HandlerFunc)
- func (a *App) Patch(path string, handlerFunc http.HandlerFunc)
- func (a *App) Post(path string, handlerFunc http.HandlerFunc)
- func (a *App) Put(path string, handlerFunc http.HandlerFunc)
- func (a *App) Run(ctx context.Context, addr string) error
- func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (a *App) Trace(path string, handlerFunc http.HandlerFunc)
- func (a *App) Use(ss ...func(http.Handler) http.Handler)
- type ClientIPResolution
- type PanicError
- type Recovery
- type RequestLogging
- type Router
Constants ¶
This section is empty.
Variables ¶
var TrustAllCIDRs = []netip.Prefix{ netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"), }
TrustAllCIDRs trusts every peer; assign to TrustedCIDRs only when a trusted proxy always overwrites the forwarding headers.
Functions ¶
func Chain ¶
Chain returns a function that composes the given wrappers into a single wrapper. Applying the returned function to a handler h returns a new handler that runs each wrapper in order: ss[0] is outermost, receives the request first, and its response is what the caller ultimately sees.
Chain(Logging, Auth)(h) is equivalent to Logging(Auth(h)). With no wrappers, Chain returns a function that leaves its argument unchanged.
func ChainFunc ¶
func ChainFunc(ss ...func(http.Handler) http.Handler) func(http.HandlerFunc) http.HandlerFunc
ChainFunc returns a function that composes the given wrappers into a single wrapper over http.HandlerFunc handlers, the counterpart of Chain for func-typed registration methods such as App.Get and App.Put.
The wrappers are the same func(http.Handler) http.Handler type as Chain's, so wrappers written for Chain work unchanged.
With no wrappers, ChainFunc returns a function that leaves its argument unchanged.
func ClientIPFromContext ¶ added in v0.2.0
ClientIPFromContext returns the client IP stored by ClientIPResolution.Handler, or "" when the request was not wrapped.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is an HTTP router built on top of http.ServeMux, extending it with method-based routing helpers such as App.Get, App.Post and App.Any. Requests are matched against registered patterns using the same syntax and precedence rules as http.ServeMux. App implements http.Handler; create one with NewApp.
func Default ¶ added in v0.2.0
func Default() *App
Default returns a new App with the standard wrappers already registered by App.Use, outermost first:
- ClientIPResolution resolves the client IP into the request context.
- RequestLogging logs each request, including the resolved client_ip.
- Recovery recovers panics raised by the handler.
The order is fixed by the wrappers themselves: ClientIPResolution must run before RequestLogging reads the client IP, and RequestLogging must sit outside Recovery so a recovered panic is logged as the 500 response it becomes. Recovery is therefore innermost, and a panic raised by ClientIPResolution.Lookup is not recovered.
Default takes no configuration; every wrapper runs with its zero-value defaults. To tune one, register the same set explicitly:
clientIP := &ClientIPResolution{
TrustedCIDRs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")},
}
app := NewApp()
app.Use(clientIP.Handler, new(RequestLogging).Handler, new(Recovery).Handler)
Each wrapper snapshots its fields when a registration method such as App.Get applies it, so configure a wrapper before registering routes.
func (*App) Any ¶
func (a *App) Any(path string, handlerFunc http.HandlerFunc)
Any registers handlerFunc for the given path, matching all HTTP methods.
func (*App) Connect ¶
func (a *App) Connect(path string, handlerFunc http.HandlerFunc)
Connect registers handlerFunc for CONNECT requests to the given path.
func (*App) Delete ¶
func (a *App) Delete(path string, handlerFunc http.HandlerFunc)
Delete registers handlerFunc for DELETE requests to the given path.
func (*App) Get ¶
func (a *App) Get(path string, handlerFunc http.HandlerFunc)
Get registers handlerFunc for GET requests to the given path.
func (*App) Group ¶
Group creates a new router group with the given relative path and invokes fn with it. Routes registered by fn are resolved relative to the group's path (see Router.Group). If fn is nil, Group does nothing.
func (*App) Handle ¶
Handle registers the handler for the given pattern, with the same behavior as http.ServeMux.Handle and http.Handle.
func (*App) HandleFunc ¶
func (a *App) HandleFunc(pattern string, handlerFunc http.HandlerFunc)
HandleFunc registers the handler function for the given pattern, with the same behavior as http.ServeMux.HandleFunc and http.HandleFunc.
func (*App) Head ¶
func (a *App) Head(path string, handlerFunc http.HandlerFunc)
Head registers handlerFunc for HEAD requests to the given path.
func (*App) Options ¶
func (a *App) Options(path string, handlerFunc http.HandlerFunc)
Options registers handlerFunc for OPTIONS requests to the given path.
func (*App) Patch ¶
func (a *App) Patch(path string, handlerFunc http.HandlerFunc)
Patch registers handlerFunc for PATCH requests to the given path.
func (*App) Post ¶
func (a *App) Post(path string, handlerFunc http.HandlerFunc)
Post registers handlerFunc for POST requests to the given path.
func (*App) Put ¶
func (a *App) Put(path string, handlerFunc http.HandlerFunc)
Put registers handlerFunc for PUT requests to the given path.
func (*App) Run ¶
Run listens on the given TCP address and serves HTTP requests until ctx is canceled or the server fails. If ctx is canceled, Run shuts the server down gracefully and returns nil.
type ClientIPResolution ¶ added in v0.2.0
type ClientIPResolution struct {
// TrustedCIDRs lists the peer CIDRs whose X-Forwarded-For and
// X-Real-IP headers are trusted. Nil or empty trusts no peer: only
// the remote address is reported. Peers are compared after folding
// IPv4-mapped IPv6 to plain IPv4, so an IPv6-only prefix such as
// "::/0" never matches IPv4 peers; to trust every peer, assign
// [TrustAllCIDRs]. Typical values are your reverse proxy's CIDRs,
// e.g. netip.MustParsePrefix("10.0.0.0/8") or
// netip.MustParsePrefix("2001:db8::/32").
TrustedCIDRs []netip.Prefix
// Lookup specifies an optional function consulted before the built-in
// resolution. A non-empty result is used as the client IP as-is,
// bypassing the trust gate; an empty result falls back to the built-in
// resolution. It can trust headers the built-in resolution does not
// read, such as CF-Connecting-IP, but callers must ensure their
// deployment overwrites the header, or a client can forge the reported
// IP.
Lookup func(*http.Request) string
}
ClientIPResolution resolves the client IP address and stores it in the request context, where RequestLogging and ClientIPFromContext read it.
The remote address is always reported. The X-Forwarded-For and X-Real-IP headers are consulted only when the peer is inside ClientIPResolution.TrustedCIDRs; a header from any other peer is ignored, since anyone can set it. For a trusted peer, the X-Forwarded-For chain is walked from the right, skipping trusted proxies, so an attacker cannot forge an entry past the last trusted hop.
func (*ClientIPResolution) Handler ¶ added in v0.2.0
func (c *ClientIPResolution) Handler(h http.Handler) http.Handler
Handler resolves the client IP for each request and stores it in the request context for ClientIPFromContext. It captures the current field values at call time; later changes do not affect the returned handler.
type PanicError ¶ added in v0.2.0
type PanicError struct {
// Value is the value passed to panic. It may not be an error.
Value any
// Stack is the goroutine stack trace captured at the recovery point.
Stack []byte
}
PanicError carries the value and stack trace of a recovered panic.
func (*PanicError) Error ¶ added in v0.2.0
func (p *PanicError) Error() string
Error implements the error interface.
func (*PanicError) LogValue ¶ added in v0.2.0
func (p *PanicError) LogValue() slog.Value
LogValue implements the slog.LogValuer interface.
func (*PanicError) Unwrap ¶ added in v0.2.0
func (p *PanicError) Unwrap() error
Unwrap returns the panic value if it is an error, enabling errors.Is, errors.As, and errors.AsType to match against it.
type Recovery ¶ added in v0.2.0
type Recovery struct {
// HandlePanic is called after a panic is recovered to handle it,
// typically by writing the HTTP response. For most panics, Recovery
// already logs the stack trace; HandlePanic only needs to take care of
// the response (and optional side-effects such as error reporting).
// Connection-related panics are handled internally and never invoke this function.
// If the response was already committed prior to the panic, net/http
// ignores further WriteHeader calls and appends further writes to the body.
// Implementations should be aware of this behavior.
// If nil, defaultHandlePanic is used.
HandlePanic func(http.ResponseWriter, *http.Request, *PanicError)
}
Recovery wraps an http.Handler to recover from panics, logging them with a stack trace and writing error responses via HandlePanic.
func (*Recovery) Handler ¶ added in v0.2.0
Handler returns a handler that recovers from panics raised while invoking h and logs them via slog.
If a panic is recovered, the error response is written by Recovery.HandlePanic (or defaultHandlePanic if it is nil). Panics whose value is or wraps http.ErrAbortHandler are re-panicked so net/http can abort the connection silently, and panics caused by a dead connection (such as a reset or a broken pipe) are logged as warnings without writing a response.
type RequestLogging ¶ added in v0.2.0
type RequestLogging struct {
// OmitBytesWritten omits the response body bytes from the logged record.
OmitBytesWritten bool
// HideQueryString omits the query string from the logged uri,
// e.g. for tokens or API keys.
HideQueryString bool
// ExtraAttrs appends attributes to each record.
ExtraAttrs func(*http.Request) []slog.Attr
}
RequestLogging logs each HTTP request via slog. If a ClientIPResolution handler wrapped the request, the resolved client IP is included as a client_ip attribute.
func (*RequestLogging) Handler ¶ added in v0.2.0
func (rl *RequestLogging) Handler(h http.Handler) http.Handler
Handler wraps h and logs each request it serves. It captures the current field values at call time; later changes do not affect the returned handler. If h panics, no record is written for that request; compose this handler outside any panic recovery so recovered panics are recorded as the error responses they become.
type Router ¶
type Router interface {
// Use registers the given wrappers and applies them to every handler
// registered after this call. Wrappers run in registration order:
// the first is outermost and receives the request first, the same
// composition as [Chain].
Use(ss ...func(http.Handler) http.Handler)
// Handle registers the handler for the given pattern, with the same
// behavior as [http.ServeMux.Handle] and [http.Handle].
Handle(pattern string, handler http.Handler)
// HandleFunc registers the handler function for the given pattern,
// with the same behavior as [http.ServeMux.HandleFunc] and [http.HandleFunc].
HandleFunc(pattern string, handler http.HandlerFunc)
// Any Get Post Delete Patch Put Options Head Connect and Trace
// register handlerFunc on the given pattern for their respective HTTP
// methods; Any matches all methods.
Any(path string, handlerFunc http.HandlerFunc)
Get(path string, handlerFunc http.HandlerFunc)
Post(path string, handlerFunc http.HandlerFunc)
Delete(path string, handlerFunc http.HandlerFunc)
Patch(path string, handlerFunc http.HandlerFunc)
Put(path string, handlerFunc http.HandlerFunc)
Options(path string, handlerFunc http.HandlerFunc)
Head(path string, handlerFunc http.HandlerFunc)
Connect(path string, handlerFunc http.HandlerFunc)
Trace(path string, handlerFunc http.HandlerFunc)
// Group creates a new router group with the given relative path.
// The fn function registers routes within the group, each of which
// is resolved relative to the group's path.
// For example, a group registered at "/api" with a route registered
// at "/users" handles requests for "/api/users".
Group(relativePath string, fn func(r Router))
}
Router is the set of core routing methods implemented by App, using only the standard net/http.