web

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

View Source
const (
	HealthPath = "/healthz"
	ReadyPath  = "/readyz"
)

The probe endpoints RegisterHealth mounts.

View Source
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"
)
View Source
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 Liveness

func Liveness() http.Handler

Liveness reports that the process is up and serving HTTP, and checks nothing else.

func Readiness

func Readiness(checks ...Check) http.Handler

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

func RegisterHealth(m Mounter, checks ...Check)

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.

func (*Config) Addr

func (c *Config) Addr() string

Addr joins the host and port; an unset port reads as 0.

func (*Config) Finalize

func (c *Config) Finalize(envPrefix string) error

Finalize composes the environment override names from envPrefix (an empty prefix disables overrides), applies defaults, applies the overrides, and validates.

func (*Config) Merge

func (c *Config) Merge(src *Config)

Merge overlays src's set fields onto the receiver.

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

func NewEnv(prefix string) Env

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

func NewGroup(prefix string) *Group

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

func (g *Group) Mount(child *Group)

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

type Middleware func(http.Handler) http.Handler

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

func NewModule(g *Group) *Module

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.

func (*Module) Prefix added in v0.4.0

func (m *Module) Prefix() string

Prefix reports the path prefix the module mounts at.

func (*Module) ServeHTTP added in v0.4.0

func (m *Module) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

type Mounter

type Mounter interface {
	Handle(pattern string, handler http.Handler)
}

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.

func (Problem) Write

func (p Problem) Write(w http.ResponseWriter) error

Write sends the problem, applying defaults: a zero Status becomes 500, an empty Type becomes about:blank, and an empty Title takes the status phrase.

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

func (r *Router) Handle(pattern string, handler http.Handler)

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

func (r *Router) Mount(m *Module)

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

func NewServer(cfg Config, handler http.Handler) *Server

NewServer builds a Server from a finalized Config and the handler it serves. It panics if cfg has not been finalized.

func (*Server) Addr

func (s *Server) Addr() string

Addr reports the bound address once started — a configured port 0 reads back its assignment — and the configured address before.

func (*Server) Err

func (s *Server) Err() <-chan error

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.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown drains the server gracefully via http.Server.Shutdown. Before a successful Start it 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.

func (*Server) Start

func (s *Server) Start(ctx context.Context) error

Start binds the listener on the calling goroutine — a bind failure is the returned error, and ctx bounds the bind — then serves in the background. A second Start returns an error.

Jump to

Keyboard shortcuts

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