Documentation
¶
Overview ¶
Package web provides the HTTP layer: a net/http server bound to a caller-supplied handler, RFC 9457 problem responses, a JSON writer, and the liveness and readiness endpoints an orchestrator probes.
Server ¶
NewServer wraps an http.Server built from a finalized Config and a handler the caller composes; an unfinalized Config panics with the fix named. Server.Start binds the listener on the calling goroutine — the context bounding the bind — and only then serves in the background, so a bind failure is returned to the caller instead of being lost in a goroutine. Server.Addr reports the bound address once started; a configured port 0 binds an ephemeral port, and Addr reads back the assignment.
Server.Err belongs to a successful serve session: after Start returns nil, a serve failure arrives on it, and the channel closes when serving stops. http.ErrServerClosed is the expected end of a shutdown and is not reported. Server.Shutdown before a successful Start is a no-op that leaves the server startable, so a lifecycle drain after a failed startup passes through cleanly; once it has served, a Server is single-use — construct a new one to serve again.
Lifecycle wiring ¶
The package registers no lifecycle hooks of its own and holds no shutdown timeout. Server.Start and Server.Shutdown carry the lifecycle package's hook signature, and Server.Err is a monitorable source, so a composition root wires the server as bare method values:
lc.OnStartup(srv.Start) lc.OnShutdown(srv.Shutdown) lc.Monitor(srv.Err())
A bind failure fails the coordinator's startup, a serve failure ends its run, and the shutdown hook receives the timeout-bounded drain context Server.Shutdown consumes directly.
Routing ¶
Group, Module, and Router compose an application's route tree. A Group declares routes: a path prefix (multi-segment prefixes such as "/api/v1" are first-class), a middleware stack, atomic routes, and nested child groups. NewModule compiles a group tree once into a Module — every route under its full pattern with its middleware baked in, group stacks outermost ordered root to leaf, then per-route middleware — and seals the tree, so a route registered after compilation panics instead of going silently dead. NewHandlerModule mounts a raw handler under a prefix (an embedded client application, a file server), the one case where a module strips the prefix from the request.
A Router dispatches to mounted modules by longest-prefix match on segment boundaries, falling back to a native http.ServeMux for every path no module owns. Router.Handle mirrors ServeMux.Handle on the native mux — *Router satisfies Mounter, so RegisterHealth mounts the probes there, structurally outside every module's middleware — and Router.Use wraps the whole dispatch. The effective order is router middleware, then group middleware root to leaf, then route middleware, then the handler. Registration mistakes — a malformed prefix, a duplicate pattern, a second module at a prefix, a sealed-group mutation — panic at wiring time; nothing recomposes or validates per request.
Configuration ¶
Config holds the host, the port, and the server's four timeouts, and implements the config package's Merge and Finalize contract, so it loads as part of an application's configuration rather than on its own. The port and timeouts are pointers: nil is unset and takes the default, while an explicit zero survives the load and means what it says — a disabled timeout, or an ephemeral port. A file and the environment express both states identically. Finalize composes its environment override names from the prefix it receives (via NewEnv, recorded on Env for introspection), applies defaults, reads the overrides, and validates; an empty prefix disables the overrides.
Health ¶
Liveness reports that the process is up and serving HTTP and checks nothing else; an unanswered probe is the liveness signal. Readiness aggregates the Check values the caller supplies — typically the lifecycle coordinator alongside each subsystem that reports readiness — and answers 503 unless every one of them is ready. A Check with a nil Checker reports not ready, so a subsystem that failed to construct fails the probe. RegisterHealth mounts both endpoints on a Mounter. The patterns use net/http.ServeMux's method-scoped syntax ("GET /healthz"), which http.ServeMux handles directly; any other Mounter translates them.
Middleware ¶
A Middleware wraps one http.Handler in another, and Chain composes a set of them around a handler in argument order: in Chain(h, a, b), a sees the request first. A nil entry is skipped, so a caller can build a chain with conditional entries without filtering it first.
RequestLogger emits one record per request through a *slog.Logger the caller supplies — method, path, status, duration, and remote address — at info level, or at error level with the panic value attached when the handler panics (the panic then continues to net/http's recovery). A successful request to HealthPath or ReadyPath logs at debug — orchestrator heartbeat, visible in development and quiet in production — while a failing probe stays at info. Beyond that the middleware does not judge status codes: whether a 5xx was the application's own failure belongs to the error mapping, not here.
The middleware wraps the ResponseWriter to capture the status. The wrapper records the first status written, implements Unwrap so flushing and hijacking work through http.ResponseController, and delegates io.ReaderFrom so a handler serving files keeps the zero-copy path. http.Pusher is not available through the wrapper.
Problem responses ¶
Error responses are RFC 9457 problem documents. The type member identifies the problem's semantics and is the member a client branches on, with title advisory and status an advisory copy of the status line. This package defines no type URIs of its own: every problem it emits is ProblemTypeBlank, and a consumer supplies its own URI through Problem.Write or the extras map of WriteProblemWith. Extras may add or override any member except status, which always matches the status line. A zero Status defaults to 500. An empty title defaults to the status phrase, and is omitted for a code outside the standard table.
Index ¶
- Constants
- func Chain(handler http.Handler, middleware ...Middleware) http.Handler
- func Liveness() http.Handler
- func Readiness(checks ...Check) http.Handler
- func RegisterHealth(m Mounter, checks ...Check)
- func WriteJSON(w http.ResponseWriter, status int, data any) error
- func WriteProblem(w http.ResponseWriter, r *http.Request, status int, title, detail string) error
- func WriteProblemWith(w http.ResponseWriter, r *http.Request, status int, title, detail string, ...) error
- type Check
- type Config
- type Env
- type Group
- type Middleware
- type Module
- type Mounter
- type Problem
- type Router
- type Server
Constants ¶
const ( HealthPath = "/healthz" ReadyPath = "/readyz" )
The probe endpoints RegisterHealth mounts.
const ( // ProblemMediaType is the RFC 9457 problem document media type. ProblemMediaType = "application/problem+json" // ProblemTypeBlank is the type of a problem with no semantics beyond its // status code. ProblemTypeBlank = "about:blank" )
const JSONMediaType = "application/json"
JSONMediaType is the media type WriteJSON sets.
Variables ¶
This section is empty.
Functions ¶
func Chain ¶
func Chain(handler http.Handler, middleware ...Middleware) http.Handler
Chain composes middleware around handler in argument order: in Chain(h, a, b), a sees the request first. A nil entry is skipped, so a caller can build a chain with conditional entries without filtering it.
func Readiness ¶
Readiness aggregates the checks: 200 with each participant's state when all are ready, and otherwise a 503 problem document naming them. Zero checks report ready.
func RegisterHealth ¶
RegisterHealth mounts Liveness at GET /healthz and Readiness over checks at GET /readyz.
func WriteJSON ¶
func WriteJSON( w http.ResponseWriter, status int, data any, ) error
WriteJSON sends data as JSON with the given status.
func WriteProblem ¶
func WriteProblem( w http.ResponseWriter, r *http.Request, status int, title, detail string, ) error
WriteProblem sends a problem for the request, with Instance set to the request path and Problem.Write's defaults applied.
func WriteProblemWith ¶
func WriteProblemWith( w http.ResponseWriter, r *http.Request, status int, title, detail string, extras map[string]any, ) error
WriteProblemWith is WriteProblem with extra members copied over the document. Extras may add or override any member except status, which always matches the status line.
Types ¶
type Check ¶
type Check struct {
Name string
Checker lifecycle.ReadinessChecker
}
Check is one named participant in the readiness aggregate. A nil Checker reports not ready, so a subsystem that failed to construct fails the probe.
type Config ¶
type Config struct {
Host string `json:"host"`
Port *int `json:"port"`
ReadTimeout *config.Duration `json:"read_timeout"`
ReadHeaderTimeout *config.Duration `json:"read_header_timeout"`
WriteTimeout *config.Duration `json:"write_timeout"`
IdleTimeout *config.Duration `json:"idle_timeout"`
Env Env `json:"-"`
}
Config holds the server's address and timeouts. The port and timeouts are tri-state pointers: nil is unset and takes the default, while an explicit zero survives the load and means what it says — a disabled timeout, or an ephemeral port. Env records the environment-variable names Finalize composed and read; it is excluded from JSON.
type Env ¶
type Env struct {
Host string
Port string
ReadTimeout string
ReadHeaderTimeout string
WriteTimeout string
IdleTimeout string
}
Env names the environment variables Config.Finalize reads, composed from the prefix it receives: SERVER_HOST, SERVER_PORT, and the four timeout names under whatever prefix config.EnvName produces. An empty name disables that one override; the zero value — an empty prefix — disables all of them. Populated by Finalize and exposed for introspection.
func NewEnv ¶
NewEnv composes the standard override names from a prefix: SERVER_HOST, SERVER_PORT, and the four timeout names under whatever prefix config.EnvName produces. An empty prefix returns the zero Env, disabling the overrides.
type Group ¶ added in v0.4.0
type Group struct {
// contains filtered or unexported fields
}
Group is a declarative route group: a path prefix, a middleware stack, atomic routes, and nested child groups. NewModule compiles a group tree into a servable Module and seals it; mutating a sealed group panics, so a route added after compilation cannot be silently dead.
func NewGroup ¶ added in v0.4.0
NewGroup returns an open group rooted at prefix. The prefix may span multiple segments ("/api/v1"); it must begin with '/' and not end with one, and a malformed prefix panics — a wiring error caught at registration.
func (*Group) Handle ¶ added in v0.4.0
func (g *Group) Handle( method, pattern string, handler http.Handler, mw ...Middleware, )
Handle registers handler for method and pattern relative to the group's prefix. An empty pattern binds the prefix itself; an empty method registers the pattern for every method. Per-route middleware wraps innermost. Handle after NewModule panics.
func (*Group) HandleFunc ¶ added in v0.4.0
func (g *Group) HandleFunc( method, pattern string, handler http.HandlerFunc, mw ...Middleware, )
HandleFunc is Group.Handle for a handler function.
func (*Group) Mount ¶ added in v0.4.0
Mount nests child under the group: the child's prefix appends to the parent's, and the parent's middleware wraps the child's. Mount after NewModule panics.
func (*Group) Use ¶ added in v0.4.0
func (g *Group) Use(mw ...Middleware)
Use appends middleware to the group's stack, wrapping every route in this group and its children. Use after NewModule panics.
type Middleware ¶
Middleware wraps one http.Handler in another.
func RequestLogger ¶
func RequestLogger(logger *slog.Logger) Middleware
RequestLogger emits one record per request — method, path, status, duration, remote address — at info level. A successful request to HealthPath or ReadyPath logs at debug, keeping orchestrator heartbeat out of production logs while a failing probe stays visible; a panicking handler logs at error with the panic value attached before the panic continues to net/http's recovery. The wrapped ResponseWriter records the first status written, implements Unwrap so http.ResponseController reaches through it, and delegates io.ReaderFrom.
type Module ¶ added in v0.4.0
type Module struct {
// contains filtered or unexported fields
}
Module is a compiled, prefix-mounted handler with its middleware baked in, dispatched by a Router.
func NewHandlerModule ¶ added in v0.4.0
func NewHandlerModule( prefix string, handler http.Handler, mw ...Middleware, ) *Module
NewHandlerModule mounts a raw handler under prefix — an embedded client application, a file server — with mw wrapped around it. The handler receives the request with the prefix stripped, via http.StripPrefix; this is the one place a module rewrites a request path.
func NewModule ¶ added in v0.4.0
NewModule compiles the group tree into a Module in one pass: every route registers on an internal mux under its full pattern with its full chain — group middleware outermost, ordered root to leaf, then per-route middleware — and the tree is sealed against further mutation. Composition happens once, here; nothing recomposes per request. A duplicate or malformed pattern panics at registration, from the mux itself.
type Mounter ¶
Mounter mounts a handler on a method-scoped pattern ("GET /healthz"); *http.ServeMux satisfies it directly.
type Problem ¶
type Problem struct {
Type string `json:"type"`
Title string `json:"title,omitempty"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
}
Problem is an RFC 9457 problem document. Type identifies the problem's semantics and is the member a client branches on; this package mints no type URIs of its own, so a consumer supplies one here or through the extras of WriteProblemWith.
type Router ¶ added in v0.4.0
type Router struct {
// contains filtered or unexported fields
}
Router dispatches to mounted Module values by longest-prefix match on segment boundaries, falling back to a native http.ServeMux for every path no module owns. Handlers registered through Router.Handle live on the native mux — structurally outside every module's middleware, which is what keeps probes clear of auth and the like — while middleware registered through Router.Use wraps the whole dispatch. Wire the router before serving; registration is not synchronized with request handling.
func NewRouter ¶ added in v0.4.0
func NewRouter() *Router
NewRouter returns a router with no modules and an empty native mux.
func (*Router) Handle ¶ added in v0.4.0
Handle mirrors http.ServeMux.Handle on the native mux, so *Router satisfies Mounter and RegisterHealth mounts the probes directly.
func (*Router) Mount ¶ added in v0.4.0
Mount adds m to the dispatch set. Mounting a second module at the same prefix panics — a wiring error caught at registration.
func (*Router) ServeHTTP ¶ added in v0.4.0
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
ServeHTTP implements http.Handler.
func (*Router) Use ¶ added in v0.4.0
func (r *Router) Use(mw ...Middleware)
Use appends middleware around the router's entire dispatch — modules and native mux alike — recomposing the chain at registration, never per request.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server wraps an http.Server whose bind and serve phases are split, so a composition root registers Server.Start and Server.Shutdown as lifecycle hooks and monitors Server.Err.
func NewServer ¶
NewServer builds a Server from a finalized Config and the handler it serves. It panics if cfg has not been finalized.
func (*Server) Addr ¶
Addr reports the bound address once started — a configured port 0 reads back its assignment — and the configured address before.
func (*Server) Err ¶
Err delivers a serve failure after a successful Start; http.ErrServerClosed is the expected end of a shutdown and is not reported. The channel closes when serving stops.