web

package
v0.4.0-dev.1 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 16 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.

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 applies defaults, then the environment overrides named by the configuration's Env value, then validates. NewEnv composes the standard names from a prefix; an empty name disables that one override, and a zero Env means no environment overrides at all.

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 names the environment variables Finalize reads; it is excluded from JSON, so only the composition root can set it.

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() error

Finalize applies defaults, applies the environment overrides named by Env, 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. An empty name disables that one override; the zero value disables all of them.

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.

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